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 pub(crate) cplusplus_macro_configured: bool,
42 include_search_roots: Vec<CppIncludeSearchRoot>,
43 driver_basename: Option<String>,
47 explicit_language: Option<CompiledLanguage>,
50 std_language: Option<CompiledLanguage>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum CompiledLanguage {
60 C,
61 Cpp,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70enum CppIncludeSearchRootKind {
71 Project,
72 Quote,
73 System,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77struct CppIncludeSearchRoot {
78 path: PathBuf,
79 kind: CppIncludeSearchRootKind,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum CppExternalIncludeResolution {
85 MissingCompileContext,
87 Undeclared,
89 Conflicting,
91 Declared { root: PathBuf, header: PathBuf },
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
105pub struct CppCompileDatabaseCoverage {
106 missing_workspace_source_count: usize,
107 missing_workspace_source_sample: Vec<PathBuf>,
108}
109
110impl CppCompileDatabaseCoverage {
111 pub fn missing_workspace_source_count(&self) -> usize {
114 self.missing_workspace_source_count
115 }
116
117 pub fn missing_workspace_source_sample(&self) -> &[PathBuf] {
119 &self.missing_workspace_source_sample
120 }
121}
122
123#[derive(Debug, Default)]
124pub struct CppCompileContexts {
125 by_source: HashMap<PathBuf, Vec<CppCompileContext>>,
126 database_sources: HashSet<PathBuf>,
132}
133
134impl CppCompileContexts {
135 pub fn load(project: &dyn Project) -> Self {
136 Self::load_with_io_error(project, |_| {})
137 }
138
139 pub fn load_with_io_error(
142 project: &dyn Project,
143 on_error: impl FnOnce(&std::io::Error),
144 ) -> Self {
145 let workspace_root = canonical_or_normalized_path(project.root());
146 let database_path = workspace_root.join(COMPILATION_DATABASE_PATH);
147 let database = match std::fs::read_to_string(database_path) {
148 Ok(database) => database,
149 Err(error) => {
150 on_error(&error);
151 return Self::default();
152 }
153 };
154 let Ok(entries) = serde_json::from_str::<Vec<CompilationDatabaseEntry>>(&database) else {
155 return Self::default();
156 };
157
158 let mut by_source: HashMap<PathBuf, Vec<CppCompileContext>> = HashMap::default();
159 let mut database_sources = HashSet::default();
160 for entry in entries {
161 let Some(source) = entry.source_path(&workspace_root) else {
162 continue;
163 };
164 let source_identity = canonical_or_normalized_path(&source);
165 if source_identity.strip_prefix(&workspace_root).is_err() {
166 continue;
167 }
168 database_sources.insert(source_identity.clone());
169 let Some(context) = entry.compile_context(&workspace_root) else {
170 continue;
171 };
172 let candidates = by_source.entry(source_identity).or_default();
178 if !candidates.contains(&context) {
179 candidates.push(context);
180 }
181 }
182 Self {
183 by_source,
184 database_sources,
185 }
186 }
187
188 pub fn missing_workspace_sources<'a>(
198 &self,
199 workspace_root: &Path,
200 workspace_files: impl IntoIterator<Item = &'a ProjectFile>,
201 ) -> CppCompileDatabaseCoverage {
202 let workspace_root = canonical_or_normalized_path(workspace_root);
203 let listed_sources = workspace_files
204 .into_iter()
205 .map(|file| canonical_or_normalized_path(&file.abs_path()))
206 .collect::<HashSet<_>>();
207 let mut missing_count = 0usize;
208 let mut sample = BTreeSet::new();
209 for source in &self.database_sources {
210 let Some(relative) = (|| {
211 let relative = source.strip_prefix(&workspace_root).ok()?;
212 (!relative.as_os_str().is_empty() && !listed_sources.contains(source))
213 .then(|| relative.to_path_buf().normalize())
214 })() else {
215 continue;
216 };
217 missing_count = missing_count.saturating_add(1);
218 sample.insert(relative);
219 if sample.len() > MAX_MISSING_WORKSPACE_SOURCE_SAMPLE {
220 sample.pop_last();
221 }
222 }
223 CppCompileDatabaseCoverage {
224 missing_workspace_source_count: missing_count,
225 missing_workspace_source_sample: sample.into_iter().collect(),
226 }
227 }
228
229 pub fn contexts_for(&self, file: &ProjectFile) -> &[CppCompileContext] {
236 self.by_source
237 .get(&canonical_or_normalized_path(&file.abs_path()))
238 .map_or(&[], Vec::as_slice)
239 }
240
241 pub fn resolve_external_angle_include(
249 &self,
250 file: &ProjectFile,
251 include: &Path,
252 ) -> CppExternalIncludeResolution {
253 self.resolve_external_include(file, &file.abs_path(), include, false)
254 }
255
256 pub fn resolve_external_include(
264 &self,
265 file: &ProjectFile,
266 including_file: &Path,
267 include: &Path,
268 quoted: bool,
269 ) -> CppExternalIncludeResolution {
270 let contexts = self.contexts_for(file);
271 let Some(first_context) = contexts.first() else {
272 return CppExternalIncludeResolution::MissingCompileContext;
273 };
274 let first =
275 first_context.resolve_external_include(file.root(), including_file, include, quoted);
276 if contexts.iter().skip(1).any(|context| {
277 context.resolve_external_include(file.root(), including_file, include, quoted) != first
278 }) {
279 return CppExternalIncludeResolution::Conflicting;
280 }
281 match first {
282 Some((root, header)) => CppExternalIncludeResolution::Declared { root, header },
283 None => CppExternalIncludeResolution::Undeclared,
284 }
285 }
286}
287
288impl CppCompileContext {
289 pub fn tu_language(&self, source_path: &Path) -> CompiledLanguage {
301 if let Some(language) = self.explicit_language {
302 return language;
303 }
304 if let Some(language) = self.std_language {
305 return language;
306 }
307 if self
308 .driver_basename
309 .as_deref()
310 .is_some_and(|driver| driver.ends_with("++"))
311 {
312 return CompiledLanguage::Cpp;
313 }
314 if source_path
315 .extension()
316 .and_then(|extension| extension.to_str())
317 == Some("c")
318 {
319 CompiledLanguage::C
320 } else {
321 CompiledLanguage::Cpp
322 }
323 }
324
325 fn external_include_roots<'a>(
326 &'a self,
327 workspace_root: &'a Path,
328 quoted: bool,
329 ) -> impl Iterator<Item = &'a Path> + 'a {
330 self.include_search_roots.iter().filter_map(move |root| {
331 (quoted || root.kind != CppIncludeSearchRootKind::Quote)
332 .then_some(root)
333 .filter(|root| {
334 root.kind == CppIncludeSearchRootKind::System
335 || !root.path.starts_with(workspace_root)
336 })
337 .map(|root| root.path.as_path())
338 })
339 }
340
341 fn resolve_external_include(
342 &self,
343 workspace_root: &Path,
344 including_file: &Path,
345 include: &Path,
346 quoted: bool,
347 ) -> Option<(PathBuf, PathBuf)> {
348 if include.is_absolute()
349 || (!quoted
350 && include
351 .components()
352 .any(|component| !matches!(component, Component::Normal(_))))
353 || (quoted
354 && include.components().any(|component| {
355 matches!(component, Component::RootDir | Component::Prefix(_))
356 }))
357 {
358 return None;
359 }
360
361 let workspace_root = canonical_or_normalized_path(workspace_root);
362 if quoted {
363 let including_directory = including_file.parent()?;
364 if let Ok(candidate) = including_directory.join(include).canonicalize()
365 && candidate.is_file()
366 {
367 if candidate.clone().normalize().starts_with(&workspace_root) {
370 return None;
371 }
372 if let Some(root) = self
373 .external_include_roots(&workspace_root, true)
374 .filter_map(|root| {
375 let root = root.canonicalize().ok()?;
376 candidate.starts_with(&root).then_some(root)
377 })
378 .next()
379 {
380 return Some((root, candidate));
381 }
382 return None;
383 }
384 }
385
386 self.external_include_roots(&workspace_root, quoted)
387 .filter_map(|root| {
388 let root = root.canonicalize().ok()?;
389 let candidate = root.join(include).canonicalize().ok()?;
390 (candidate.starts_with(&root) && candidate.is_file()).then_some((root, candidate))
391 })
392 .next()
393 }
394}
395
396#[derive(Debug, Deserialize)]
397struct CompilationDatabaseEntry {
398 directory: PathBuf,
399 file: PathBuf,
400 arguments: Option<Vec<String>>,
401 command: Option<String>,
402}
403
404impl CompilationDatabaseEntry {
405 fn source_path(&self, workspace_root: &Path) -> Option<PathBuf> {
406 absolute_path(
407 &command_directory(workspace_root, &self.directory)?,
408 &self.file,
409 )
410 }
411
412 fn compile_context(&self, workspace_root: &Path) -> Option<CppCompileContext> {
413 let arguments = match &self.arguments {
414 Some(arguments) if !arguments.is_empty() => arguments.clone(),
415 Some(_) => return None,
416 None => shlex::split(self.command.as_deref()?)?,
417 };
418 parse_compile_arguments(
419 &command_directory(workspace_root, &self.directory)?,
420 &arguments,
421 )
422 }
423}
424
425fn command_directory(workspace_root: &Path, directory: &Path) -> Option<PathBuf> {
426 absolute_path(workspace_root, directory)
427}
428
429fn parse_compile_arguments(directory: &Path, arguments: &[String]) -> Option<CppCompileContext> {
430 if arguments.is_empty() {
431 return None;
432 }
433
434 let driver_basename = driver_basename(&arguments[0]);
435 let mut project_include_roots = Vec::new();
436 let mut system_include_roots = Vec::new();
437 let mut forced_includes = Vec::new();
438 let mut defined_macros = HashSet::default();
439 let mut cplusplus_macro_configured = false;
440 let mut include_search_roots = Vec::new();
441 let mut explicit_language = None;
442 let mut std_language = None;
443 let mut index = 1;
444 while index < arguments.len() {
445 let argument = &arguments[index];
446 match argument.as_str() {
447 "-x" => {
448 if let Some(language) = language_from_x_value(arguments.get(index + 1)?) {
449 explicit_language = Some(language);
450 }
451 index += 2;
452 }
453 "/TC" => {
454 explicit_language = Some(CompiledLanguage::C);
455 index += 1;
456 }
457 "/TP" => {
458 explicit_language = Some(CompiledLanguage::Cpp);
459 index += 1;
460 }
461 "-I" | "/I" => {
462 let path = argument_path(directory, arguments.get(index + 1)?)?;
463 project_include_roots.push(path.clone());
464 include_search_roots.push(CppIncludeSearchRoot {
465 path,
466 kind: CppIncludeSearchRootKind::Project,
467 });
468 index += 2;
469 }
470 "-iquote" => {
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::Quote,
476 });
477 index += 2;
478 }
479 "-isystem" | "/external:I" | "/imsvc" => {
480 let path = argument_path(directory, arguments.get(index + 1)?)?;
481 system_include_roots.push(path.clone());
482 include_search_roots.push(CppIncludeSearchRoot {
483 path,
484 kind: CppIncludeSearchRootKind::System,
485 });
486 index += 2;
487 }
488 "-include" => {
489 forced_includes.push(argument_path(directory, arguments.get(index + 1)?)?);
490 index += 2;
491 }
492 "-D" => {
493 let name = macro_name(arguments.get(index + 1)?)?;
494 cplusplus_macro_configured |= name == "__cplusplus";
495 defined_macros.insert(name);
496 index += 2;
497 }
498 "-U" => {
503 let name = macro_name(arguments.get(index + 1)?)?;
504 cplusplus_macro_configured |= name == "__cplusplus";
505 defined_macros.remove(&name);
506 index += 2;
507 }
508 _ => {
509 if let Some(path) = argument
510 .strip_prefix("/external:I")
511 .or_else(|| argument.strip_prefix("/imsvc"))
512 {
513 let path = argument_path(directory, path)?;
514 system_include_roots.push(path.clone());
515 include_search_roots.push(CppIncludeSearchRoot {
516 path,
517 kind: CppIncludeSearchRootKind::System,
518 });
519 } else if let Some(path) = argument
520 .strip_prefix("-I")
521 .or_else(|| argument.strip_prefix("/I"))
522 {
523 let path = argument_path(directory, path)?;
524 project_include_roots.push(path.clone());
525 include_search_roots.push(CppIncludeSearchRoot {
526 path,
527 kind: CppIncludeSearchRootKind::Project,
528 });
529 } else if let Some(path) = argument.strip_prefix("-iquote") {
530 let path = argument_path(directory, path)?;
531 project_include_roots.push(path.clone());
532 include_search_roots.push(CppIncludeSearchRoot {
533 path,
534 kind: CppIncludeSearchRootKind::Quote,
535 });
536 } else if let Some(path) = argument.strip_prefix("-isystem") {
537 let path = argument_path(directory, path)?;
538 system_include_roots.push(path.clone());
539 include_search_roots.push(CppIncludeSearchRoot {
540 path,
541 kind: CppIncludeSearchRootKind::System,
542 });
543 } else if let Some(definition) = argument.strip_prefix("-D") {
544 let name = macro_name(definition)?;
545 cplusplus_macro_configured |= name == "__cplusplus";
546 defined_macros.insert(name);
547 } else if let Some(name) = argument.strip_prefix("-U") {
548 let name = macro_name(name)?;
549 cplusplus_macro_configured |= name == "__cplusplus";
550 defined_macros.remove(&name);
551 } else if let Some(value) = argument.strip_prefix("-std=") {
552 std_language = Some(if value.contains("++") {
553 CompiledLanguage::Cpp
554 } else {
555 CompiledLanguage::C
556 });
557 } else if let Some(value) = argument.strip_prefix("-x")
558 && let Some(language) = language_from_x_value(value)
559 {
560 explicit_language = Some(language);
561 }
562 index += 1;
563 }
564 }
565 }
566
567 Some(CppCompileContext {
568 project_include_roots,
569 system_include_roots,
570 forced_includes,
571 defined_macros,
572 cplusplus_macro_configured,
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#[derive(Debug, Clone)]
659pub struct CppCompileContextView {
660 contexts: std::sync::Arc<CppCompileContexts>,
661 source: PathBuf,
662}
663impl std::ops::Deref for CppCompileContextView {
664 type Target = [CppCompileContext];
665 fn deref(&self) -> &Self::Target {
666 self.contexts
667 .by_source
668 .get(&self.source)
669 .map_or(&[], Vec::as_slice)
670 }
671}
672impl CppCompileContexts {
673 pub fn shared_contexts_for(
674 self: &std::sync::Arc<Self>,
675 file: &ProjectFile,
676 ) -> CppCompileContextView {
677 CppCompileContextView {
678 contexts: std::sync::Arc::clone(self),
679 source: canonical_or_normalized_path(&file.abs_path()),
680 }
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::{CompiledLanguage, CppCompileContexts, CppExternalIncludeResolution};
687 use brokk_bifrost_core::analyzer::project::{FilesystemProject, Project, TestProject};
688 use brokk_bifrost_core::analyzer::{Language, ProjectFile};
689 use brokk_bifrost_test_macros::hardwon;
690 use std::path::{Path, PathBuf};
691
692 fn context(arguments: &[&str]) -> super::CppCompileContext {
696 let arguments = arguments
697 .iter()
698 .map(|argument| argument.to_string())
699 .collect::<Vec<_>>();
700 super::parse_compile_arguments(Path::new("/workspace"), &arguments)
701 .expect("non-empty argument vector parses")
702 }
703
704 fn project_with_database(database: Option<&str>) -> (tempfile::TempDir, TestProject) {
705 let temp = tempfile::tempdir().expect("temp dir");
706 let root = temp.path().canonicalize().expect("canonical root");
707 ProjectFile::new(root.clone(), "src/main.cpp")
708 .write("int main() { return 0; }")
709 .expect("source");
710 if let Some(database) = database {
711 ProjectFile::new(root.clone(), "compile_commands.json")
712 .write(database)
713 .expect("database");
714 }
715 (temp, TestProject::new(root, Language::Cpp))
716 }
717
718 #[test]
719 fn arguments_entry_collects_include_paths_and_macro_names() {
720 let (_temp, project) = project_with_database(Some(
721 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-iquotequotes","-isystem","system-include","-DDEBUG=1","-D","FEATURE=a=b","-c","src/main.cpp"]}]"#,
722 ));
723 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
724 let contexts = CppCompileContexts::load(&project);
725 let [context] = contexts.contexts_for(&file) else {
726 panic!("one matching context");
727 };
728
729 assert_eq!(
730 vec![
731 project.root_path().join("include"),
732 project.root_path().join("quotes"),
733 ],
734 context.project_include_roots
735 );
736 assert_eq!(
737 vec![project.root_path().join("system-include")],
738 context.system_include_roots
739 );
740 assert!(context.defined_macros.contains("DEBUG"));
741 assert!(context.defined_macros.contains("FEATURE"));
742 assert!(!context.defined_macros.contains("FEATURE=a"));
743 }
744
745 #[test]
746 fn quoted_command_entry_is_tokenized_without_executing_it() {
747 let (_temp, project) = project_with_database(Some(
748 r#"[{"directory":".","file":"src/main.cpp","command":"clang++ -I 'project include' -DNAME=\\\"two words\\\" -c src/main.cpp"}]"#,
749 ));
750 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
751 let contexts = CppCompileContexts::load(&project);
752 let [context] = contexts.contexts_for(&file) else {
753 panic!("one matching context");
754 };
755
756 assert_eq!(
757 vec![project.root_path().join("project include")],
758 context.project_include_roots
759 );
760 assert!(context.defined_macros.contains("NAME"));
761 }
762
763 #[test]
764 #[hardwon("#2011: conditional C visibility ignored command-order macro state")]
765 fn undefine_flags_apply_in_command_order() {
766 let context = context(&[
767 "cc",
768 "-DKEPT",
769 "-DCANCELLED",
770 "-U",
771 "CANCELLED",
772 "-UREVIVED",
773 "-DREVIVED",
774 "-UNEVER_DEFINED",
775 ]);
776 assert!(context.defined_macros.contains("KEPT"));
777 assert!(context.defined_macros.contains("REVIVED"));
778 assert!(!context.defined_macros.contains("CANCELLED"));
779 assert!(!context.defined_macros.contains("NEVER_DEFINED"));
780 assert!(!context.cplusplus_macro_configured);
781 for flags in [
782 vec!["c++", "-U__cplusplus"],
783 vec!["c++", "-U", "__cplusplus"],
784 vec!["c++", "-D__cplusplus=1", "-U__cplusplus"],
785 vec!["c++", "-U__cplusplus", "-D", "__cplusplus=1"],
786 ] {
787 let configured = super::parse_compile_arguments(
788 Path::new("."),
789 &flags
790 .iter()
791 .map(|flag| (*flag).to_string())
792 .collect::<Vec<_>>(),
793 )
794 .expect("compiler flags");
795 assert!(configured.cplusplus_macro_configured, "{flags:?}");
796 assert_eq!(
797 configured.defined_macros.contains("__cplusplus"),
798 flags.last() == Some(&"__cplusplus=1"),
799 "{flags:?}"
800 );
801 }
802 }
803
804 #[test]
805 fn a_file_compiled_in_two_configurations_keeps_both() {
806 let (_temp, project) = project_with_database(Some(
807 r#"[
808 {"directory":".","file":"src/main.cpp","arguments":["clang++","-c","src/main.cpp"]},
809 {"directory":".","file":"src/main.cpp","arguments":["clang++","-DOTHER","-c","src/main.cpp"]},
810 {"directory":".","file":"src/other.cpp","arguments":["clang++","-c","src/other.cpp"]}
811 ]"#,
812 ));
813 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
814 let contexts = CppCompileContexts::load(&project);
815 let candidates = contexts.contexts_for(&file);
816
817 assert_eq!(2, candidates.len());
820 assert!(candidates[0].defined_macros.is_empty());
821 assert!(candidates[1].defined_macros.contains("OTHER"));
822 }
823
824 #[test]
825 fn repeated_identical_entries_are_one_configuration() {
826 let (_temp, project) = project_with_database(Some(
827 r#"[
828 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]},
829 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]}
830 ]"#,
831 ));
832 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
833 let contexts = CppCompileContexts::load(&project);
834
835 assert_eq!(1, contexts.contexts_for(&file).len());
838 }
839
840 #[test]
841 fn missing_workspace_sources_are_canonical_bounded_and_deterministic() {
842 let (_temp, project) = project_with_database(None);
843 let root = project.root_path().to_path_buf();
844 ProjectFile::new(root.clone(), "src/present.cpp")
845 .write("int present() { return 0; }")
846 .expect("present source");
847 ProjectFile::new(root.clone(), "src/present.c")
848 .write("int present_c(void) { return 0; }")
849 .expect("present C source");
850 ProjectFile::new(root.clone(), "include/present.hin")
851 .write("int generated_declaration(void);")
852 .expect("present C header template");
853
854 let mut entries = vec![serde_json::json!({
855 "directory": ".",
856 "file": "src/./present.cpp",
857 "arguments": ["clang++", "-c", "src/present.cpp"]
858 })];
859 entries.push(serde_json::json!({
860 "directory": ".",
861 "file": "src/present.c",
862 "arguments": ["clang", "-c", "src/present.c"]
863 }));
864 entries.push(serde_json::json!({
865 "directory": ".",
866 "file": "include/present.hin",
867 "arguments": ["clang", "-x", "c-header", "include/present.hin"]
868 }));
869 entries.extend(
870 (0..(super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE + 3)).map(|index| {
871 serde_json::json!({
872 "directory": ".",
873 "file": format!("generated/./unit-{index:02}.cpp"),
874 "arguments": ["clang++", "-c", format!("generated/unit-{index:02}.cpp")]
875 })
876 }),
877 );
878 entries.push(serde_json::json!({
881 "directory": "generated",
882 "file": "../generated/unit-00.cpp",
883 "arguments": ["clang++", "-c", "../generated/unit-00.cpp"]
884 }));
885 ProjectFile::new(root.clone(), "compile_commands.json")
886 .write(serde_json::to_string(&entries).expect("database JSON"))
887 .expect("database");
888
889 let contexts = CppCompileContexts::load(&project);
890 let listed = project
891 .analyzable_files(Language::Cpp)
892 .expect("C++ listing");
893 let coverage = contexts.missing_workspace_sources(&root, &listed);
894
895 assert_eq!(
896 super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE + 3,
897 coverage.missing_workspace_source_count()
898 );
899 assert_eq!(
900 (0..super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE)
901 .map(|index| Path::new("generated").join(format!("unit-{index:02}.cpp")))
902 .collect::<Vec<_>>(),
903 coverage.missing_workspace_source_sample()
904 );
905 assert!(
906 !coverage
907 .missing_workspace_source_sample()
908 .contains(&PathBuf::from("src/present.cpp"))
909 );
910
911 let reversed = entries.into_iter().rev().collect::<Vec<_>>();
912 ProjectFile::new(root.clone(), "compile_commands.json")
913 .write(serde_json::to_string(&reversed).expect("reversed database JSON"))
914 .expect("reversed database");
915 let reversed_coverage =
916 CppCompileContexts::load(&project).missing_workspace_sources(&root, &listed);
917 assert_eq!(coverage, reversed_coverage);
918 }
919
920 #[test]
921 fn missing_workspace_sources_excludes_outside_root_and_ignored_files() {
922 let temp = tempfile::tempdir().expect("workspace root");
923 let root = temp.path().canonicalize().expect("canonical root");
924 let outside = tempfile::tempdir().expect("outside root");
925 let outside_source = outside
926 .path()
927 .canonicalize()
928 .expect("canonical outside root")
929 .join("outside.cpp")
930 .to_string_lossy()
931 .into_owned();
932 ProjectFile::new(root.clone(), "src/present.cpp")
933 .write("int present() { return 0; }")
934 .expect("present source");
935 ProjectFile::new(root.clone(), "ignored.cpp")
936 .write("int ignored() { return 0; }")
937 .expect("ignored source");
938 ProjectFile::new(root.clone(), ".bifrostignore")
939 .write("ignored.cpp\n")
940 .expect("ignore file");
941 let database = serde_json::json!([
942 {
943 "directory": ".",
944 "file": "src/../src/present.cpp",
945 "arguments": ["clang++", "-c", "src/present.cpp"]
946 },
947 {
948 "directory": ".",
949 "file": "ignored.cpp",
950 "arguments": ["clang++", "-c", "ignored.cpp"]
951 },
952 {
953 "directory": ".",
954 "file": outside_source.clone(),
955 "arguments": ["clang++", "-c", outside_source]
956 }
957 ]);
958 ProjectFile::new(root.clone(), "compile_commands.json")
959 .write(database.to_string())
960 .expect("database");
961 let project = FilesystemProject::new(root.clone()).expect("filesystem project");
962 let listed = project
963 .analyzable_files(Language::Cpp)
964 .expect("C++ listing");
965 assert!(
966 listed
967 .iter()
968 .any(|file| file.rel_path() == Path::new("src/present.cpp"))
969 );
970 assert!(
971 !listed
972 .iter()
973 .any(|file| file.rel_path() == Path::new("ignored.cpp"))
974 );
975
976 let coverage = CppCompileContexts::load(&project).missing_workspace_sources(&root, &listed);
977 assert_eq!(1, coverage.missing_workspace_source_count());
978 assert_eq!(
979 &[PathBuf::from("ignored.cpp")],
980 coverage.missing_workspace_source_sample()
981 );
982 }
983
984 #[test]
985 fn a_quoted_include_checks_the_including_directory_before_external_roots() {
986 let external = tempfile::tempdir().expect("external root");
987 let external_root = external
988 .path()
989 .canonicalize()
990 .expect("canonical external root");
991 std::fs::write(external_root.join("vendor.hpp"), "class External {};\n")
992 .expect("external vendor header");
993 std::fs::write(external_root.join("from-search.hpp"), "class Search {};\n")
994 .expect("external search header");
995 let database = serde_json::json!([{
996 "directory": ".",
997 "file": "src/main.cpp",
998 "arguments": [
999 "clang++",
1000 "-I",
1001 external_root,
1002 "-c",
1003 "src/main.cpp"
1004 ]
1005 }])
1006 .to_string();
1007 let (_temp, project) = project_with_database(Some(&database));
1008 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1009 let source_path = file.abs_path();
1010 let source_directory = source_path.parent().expect("source directory");
1011 std::fs::create_dir_all(source_directory).expect("source directory");
1012 std::fs::write(source_directory.join("vendor.hpp"), "class Local {};\n")
1013 .expect("local vendor header");
1014 let contexts = CppCompileContexts::load(&project);
1015
1016 assert_eq!(
1017 CppExternalIncludeResolution::Undeclared,
1018 contexts.resolve_external_include(
1019 &file,
1020 &file.abs_path(),
1021 std::path::Path::new("vendor.hpp"),
1022 true,
1023 )
1024 );
1025 assert!(matches!(
1026 contexts.resolve_external_include(
1027 &file,
1028 &file.abs_path(),
1029 std::path::Path::new("from-search.hpp"),
1030 true,
1031 ),
1032 CppExternalIncludeResolution::Declared { .. }
1033 ));
1034 assert!(matches!(
1035 contexts.resolve_external_include(
1036 &file,
1037 &file.abs_path(),
1038 std::path::Path::new("vendor.hpp"),
1039 false,
1040 ),
1041 CppExternalIncludeResolution::Declared { .. }
1042 ));
1043 }
1044
1045 #[test]
1046 fn configurations_must_agree_on_the_external_header() {
1047 let first = tempfile::tempdir().expect("first root");
1048 let second = tempfile::tempdir().expect("second root");
1049 let first = first.path().canonicalize().expect("canonical first root");
1050 let second = second.path().canonicalize().expect("canonical second root");
1051 std::fs::write(first.join("vector"), "namespace std { class vector {}; }")
1052 .expect("first header");
1053 std::fs::write(second.join("vector"), "namespace std { class vector {}; }")
1054 .expect("second header");
1055 let database = serde_json::json!([
1056 {
1057 "directory": ".",
1058 "file": "src/main.cpp",
1059 "arguments": ["clang++", "-isystem", first, "-c", "src/main.cpp"]
1060 },
1061 {
1062 "directory": ".",
1063 "file": "src/main.cpp",
1064 "arguments": ["clang++", "-isystem", second, "-c", "src/main.cpp"]
1065 }
1066 ])
1067 .to_string();
1068 let (_temp, project) = project_with_database(Some(&database));
1069 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1070
1071 assert_eq!(
1072 CppExternalIncludeResolution::Conflicting,
1073 CppCompileContexts::load(&project)
1074 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
1075 );
1076 }
1077
1078 #[test]
1079 fn external_include_cannot_escape_its_declared_root() {
1080 let external = tempfile::tempdir().expect("external root");
1081 let root = external.path().canonicalize().expect("canonical root");
1082 std::fs::create_dir_all(root.join("include")).expect("include directory");
1083 std::fs::write(root.join("outside.hpp"), "class Outside {};").expect("outside header");
1084 let database = serde_json::json!([{
1085 "directory": ".",
1086 "file": "src/main.cpp",
1087 "arguments": [
1088 "clang++",
1089 "-isystem",
1090 root.join("include"),
1091 "-c",
1092 "src/main.cpp"
1093 ]
1094 }])
1095 .to_string();
1096 let (_temp, project) = project_with_database(Some(&database));
1097 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1098
1099 assert_eq!(
1100 CppExternalIncludeResolution::Undeclared,
1101 CppCompileContexts::load(&project)
1102 .resolve_external_angle_include(&file, std::path::Path::new("../outside.hpp"))
1103 );
1104 assert_eq!(
1105 CppExternalIncludeResolution::Undeclared,
1106 CppCompileContexts::load(&project)
1107 .resolve_external_angle_include(&file, &root.join("outside.hpp"))
1108 );
1109 }
1110
1111 #[test]
1112 fn msvc_project_and_system_include_flags_preserve_search_order() {
1113 let (_temp, project) = project_with_database(Some(
1114 r#"[{"directory":".","file":"src/main.cpp","arguments":["cl.exe","/I","vendor/include","/external:Ifake-system/include","/imsvc","toolchain/include","/c","src/main.cpp"]}]"#,
1115 ));
1116 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1117 let contexts = CppCompileContexts::load(&project);
1118 let [context] = contexts.contexts_for(&file) else {
1119 panic!("one MSVC compile context");
1120 };
1121
1122 assert_eq!(1, context.project_include_roots.len());
1123 assert_eq!(2, context.system_include_roots.len());
1124 }
1125
1126 #[test]
1127 fn driver_basename_ending_in_plus_plus_selects_cpp_over_a_c_extension() {
1128 let cx = context(&["clang++", "-c", "file.c"]);
1129 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1130
1131 let cx = context(&["arm-none-eabi-gcc", "-c", "file.c"]);
1134 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1135
1136 let cx = context(&["arm-none-eabi-g++", "-c", "file.h"]);
1138 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.h")));
1139 }
1140
1141 #[test]
1142 fn driver_exe_suffix_is_stripped_before_the_plus_plus_check() {
1143 let cx = context(&["clang++.exe", "-c", "file.c"]);
1144 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1145 }
1146
1147 #[test]
1148 fn std_family_outranks_the_driver_basename() {
1149 let cx = context(&["g++", "-std=gnu11", "-c", "file.c"]);
1152 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1153
1154 let cx = context(&["gcc", "-std=c++17", "-c", "file.cc"]);
1155 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
1156
1157 let cx = context(&["gcc", "-std=c17", "-c", "file.cc"]);
1158 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cc")));
1159
1160 let cx = context(&["gcc", "-std=gnu++20", "-c", "file.c"]);
1161 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1162 }
1163
1164 #[test]
1165 fn explicit_x_split_form_outranks_std_and_driver() {
1166 let cx = context(&["clang", "-x", "c", "-std=c++17", "-c", "file.cpp"]);
1167 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1168 }
1169
1170 #[test]
1171 fn explicit_x_glued_form_is_recognized() {
1172 let cx = context(&["gcc", "-xc++", "-c", "file.c"]);
1173 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1174
1175 let cx = context(&["g++", "-xc", "-c", "file.cpp"]);
1176 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1177 }
1178
1179 #[test]
1180 fn explicit_x_on_a_c_file_overrides_the_extension() {
1181 let cx = context(&["gcc", "-x", "c++", "-c", "file.c"]);
1184 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1185 }
1186
1187 #[test]
1188 fn msvc_tc_and_tp_force_the_language_regardless_of_extension() {
1189 let cx = context(&["cl.exe", "/TC", "/c", "file.cpp"]);
1190 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1191
1192 let cx = context(&["cl.exe", "/TP", "/c", "file.c"]);
1193 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1194 }
1195
1196 #[test]
1197 fn missing_or_malformed_database_has_no_context() {
1198 let (_temp, project) = project_with_database(None);
1199 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1200 assert!(
1201 CppCompileContexts::load(&project)
1202 .contexts_for(&file)
1203 .is_empty()
1204 );
1205
1206 let (_temp, project) = project_with_database(Some("not json"));
1207 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1208 assert!(
1209 CppCompileContexts::load(&project)
1210 .contexts_for(&file)
1211 .is_empty()
1212 );
1213 }
1214
1215 #[test]
1216 fn an_explicit_system_root_declares_an_angle_include() {
1217 let (_temp, project) = project_with_database(Some(
1218 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-isystem","fake-system/include","-c","src/main.cpp"]}]"#,
1219 ));
1220 let header = ProjectFile::new(
1221 project.root_path().to_path_buf(),
1222 "fake-system/include/vector",
1223 );
1224 header
1225 .write("namespace std { class vector {}; }")
1226 .expect("header");
1227 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1228
1229 assert_eq!(
1230 CppExternalIncludeResolution::Declared {
1231 root: project
1232 .root_path()
1233 .join("fake-system/include")
1234 .canonicalize()
1235 .expect("canonical root"),
1236 header: header.abs_path().canonicalize().expect("canonical header"),
1237 },
1238 CppCompileContexts::load(&project)
1239 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
1240 );
1241 }
1242
1243 #[test]
1244 fn an_external_project_root_declares_but_a_workspace_project_root_does_not() {
1245 let external = tempfile::tempdir().expect("external root");
1246 let external_root = external
1247 .path()
1248 .canonicalize()
1249 .expect("canonical external root");
1250 std::fs::write(external_root.join("vendor.hpp"), "class Vendor {};")
1251 .expect("external header");
1252 let database = serde_json::json!([{
1253 "directory": ".",
1254 "file": "src/main.cpp",
1255 "arguments": [
1256 "clang++",
1257 "-I",
1258 external_root,
1259 "-I",
1260 "include",
1261 "-c",
1262 "src/main.cpp"
1263 ]
1264 }])
1265 .to_string();
1266 let (_temp, project) = project_with_database(Some(&database));
1267 ProjectFile::new(project.root_path().to_path_buf(), "include/local.hpp")
1268 .write("class Local {};")
1269 .expect("local header");
1270 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1271 let contexts = CppCompileContexts::load(&project);
1272
1273 assert!(matches!(
1274 contexts.resolve_external_angle_include(&file, std::path::Path::new("vendor.hpp")),
1275 CppExternalIncludeResolution::Declared { .. }
1276 ));
1277 assert_eq!(
1278 CppExternalIncludeResolution::Undeclared,
1279 contexts.resolve_external_angle_include(&file, std::path::Path::new("local.hpp"))
1280 );
1281 }
1282
1283 #[test]
1284 fn default_by_extension_c_is_c_and_everything_else_is_cpp() {
1285 let cx = context(&["cc", "-c", "file.c"]);
1286 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1287
1288 let cx = context(&["cc", "-c", "file.cc"]);
1289 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
1290 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.hpp")));
1291 let cx = context(&["cl.exe", "/c", "file.c"]);
1292 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1293
1294 let cx = context(&["cl.exe", "/c", "file.cpp"]);
1295 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cpp")));
1296 }
1297
1298 #[test]
1299 #[hardwon("#6929: a Windows verbatim prefix made an in-workspace sibling external")]
1300 fn quoted_sibling_external_header_respects_workspace_boundary() {
1301 let outer = tempfile::tempdir().unwrap();
1304 let outer_root = outer.path().canonicalize().unwrap();
1305 let workspace = outer_root.join("workspace");
1306 let external = outer_root.join("external");
1307 std::fs::create_dir_all(&workspace).unwrap();
1308 std::fs::create_dir_all(&external).unwrap();
1309 for directory in [&workspace, &external] {
1310 std::fs::write(directory.join("caller.hpp"), "#include \"sibling.hpp\"\n").unwrap();
1311 std::fs::write(directory.join("sibling.hpp"), "struct S {};\n").unwrap();
1312 }
1313 let cx = super::parse_compile_arguments(
1316 &workspace,
1317 &[
1318 "clang++".into(),
1319 "-I".into(),
1320 outer_root.to_string_lossy().into_owned(),
1321 ],
1322 )
1323 .unwrap();
1324 assert_eq!(
1325 cx.resolve_external_include(
1326 &workspace,
1327 &workspace.join("caller.hpp"),
1328 Path::new("sibling.hpp"),
1329 true
1330 ),
1331 None
1332 );
1333 assert_eq!(
1334 cx.resolve_external_include(
1335 &workspace,
1336 &external.join("caller.hpp"),
1337 Path::new("sibling.hpp"),
1338 true
1339 ),
1340 Some((outer_root, external.join("sibling.hpp")))
1341 );
1342 }
1343}