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::path::{Component, Path, PathBuf};
13
14const COMPILATION_DATABASE_PATH: &str = "compile_commands.json";
15
16pub fn is_cpp_compile_context_input(file: &ProjectFile) -> bool {
19 file.rel_path() == Path::new(COMPILATION_DATABASE_PATH)
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct CppCompileContext {
28 pub project_include_roots: Vec<PathBuf>,
29 pub system_include_roots: Vec<PathBuf>,
30 pub forced_includes: Vec<PathBuf>,
31 pub defined_macros: HashSet<String>,
32 include_search_roots: Vec<CppIncludeSearchRoot>,
33 driver_basename: Option<String>,
37 explicit_language: Option<CompiledLanguage>,
40 std_language: Option<CompiledLanguage>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum CompiledLanguage {
50 C,
51 Cpp,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60enum CppIncludeSearchRootKind {
61 Project,
62 Quote,
63 System,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67struct CppIncludeSearchRoot {
68 path: PathBuf,
69 kind: CppIncludeSearchRootKind,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum CppExternalIncludeResolution {
75 MissingCompileContext,
77 Undeclared,
79 Conflicting,
81 Declared { root: PathBuf, header: PathBuf },
83}
84
85#[derive(Debug, Default)]
86pub struct CppCompileContexts {
87 by_source: HashMap<PathBuf, Vec<CppCompileContext>>,
88}
89
90impl CppCompileContexts {
91 pub fn load(project: &dyn Project) -> Self {
92 let database_path = project.root().join(COMPILATION_DATABASE_PATH);
93 let Ok(database) = std::fs::read_to_string(database_path) else {
94 return Self::default();
95 };
96 let Ok(entries) = serde_json::from_str::<Vec<CompilationDatabaseEntry>>(&database) else {
97 return Self::default();
98 };
99
100 let mut by_source: HashMap<PathBuf, Vec<CppCompileContext>> = HashMap::default();
101 for entry in entries {
102 let Some(source) = entry.source_path(project.root()) else {
103 continue;
104 };
105 if !source.starts_with(project.root()) {
106 continue;
107 }
108 let Some(context) = entry.compile_context(project.root()) else {
109 continue;
110 };
111 let candidates = by_source.entry(source).or_default();
117 if !candidates.contains(&context) {
118 candidates.push(context);
119 }
120 }
121 Self { by_source }
122 }
123
124 pub fn contexts_for(&self, file: &ProjectFile) -> &[CppCompileContext] {
131 self.by_source
132 .get(&file.abs_path().normalize())
133 .map_or(&[], Vec::as_slice)
134 }
135
136 pub fn resolve_external_angle_include(
144 &self,
145 file: &ProjectFile,
146 include: &Path,
147 ) -> CppExternalIncludeResolution {
148 let contexts = self.contexts_for(file);
149 let Some(first_context) = contexts.first() else {
150 return CppExternalIncludeResolution::MissingCompileContext;
151 };
152 let first = first_context.resolve_external_angle_include(file.root(), include);
153 if contexts
154 .iter()
155 .skip(1)
156 .any(|context| context.resolve_external_angle_include(file.root(), include) != first)
157 {
158 return CppExternalIncludeResolution::Conflicting;
159 }
160 match first {
161 Some((root, header)) => CppExternalIncludeResolution::Declared { root, header },
162 None => CppExternalIncludeResolution::Undeclared,
163 }
164 }
165
166 pub fn external_angle_include_roots(&self, workspace_root: &Path) -> Vec<PathBuf> {
173 let mut roots = self
174 .by_source
175 .values()
176 .flatten()
177 .flat_map(|context| context.external_angle_include_roots(workspace_root))
178 .map(Path::to_path_buf)
179 .collect::<Vec<_>>();
180 roots.sort();
181 roots.dedup();
182 roots
183 }
184}
185
186impl CppCompileContext {
187 pub fn tu_language(&self, source_path: &Path) -> CompiledLanguage {
199 if let Some(language) = self.explicit_language {
200 return language;
201 }
202 if let Some(language) = self.std_language {
203 return language;
204 }
205 if self
206 .driver_basename
207 .as_deref()
208 .is_some_and(|driver| driver.ends_with("++"))
209 {
210 return CompiledLanguage::Cpp;
211 }
212 if source_path
213 .extension()
214 .and_then(|extension| extension.to_str())
215 == Some("c")
216 {
217 CompiledLanguage::C
218 } else {
219 CompiledLanguage::Cpp
220 }
221 }
222
223 pub fn external_angle_include_roots<'a>(
225 &'a self,
226 workspace_root: &'a Path,
227 ) -> impl Iterator<Item = &'a Path> + 'a {
228 self.include_search_roots.iter().filter_map(move |root| {
229 (root.kind != CppIncludeSearchRootKind::Quote
230 && (root.kind == CppIncludeSearchRootKind::System
231 || !root.path.starts_with(workspace_root)))
232 .then_some(root.path.as_path())
233 })
234 }
235
236 fn resolve_external_angle_include(
237 &self,
238 workspace_root: &Path,
239 include: &Path,
240 ) -> Option<(PathBuf, PathBuf)> {
241 if include.is_absolute()
242 || include
243 .components()
244 .any(|component| !matches!(component, Component::Normal(_)))
245 {
246 return None;
247 }
248 self.external_angle_include_roots(workspace_root)
249 .filter_map(|root| {
250 let root = root.canonicalize().ok()?;
251 let candidate = root.join(include).canonicalize().ok()?;
252 (candidate.starts_with(&root) && candidate.is_file()).then_some((root, candidate))
253 })
254 .next()
255 }
256}
257
258#[derive(Debug, Deserialize)]
259struct CompilationDatabaseEntry {
260 directory: PathBuf,
261 file: PathBuf,
262 arguments: Option<Vec<String>>,
263 command: Option<String>,
264}
265
266impl CompilationDatabaseEntry {
267 fn source_path(&self, workspace_root: &Path) -> Option<PathBuf> {
268 absolute_path(
269 &command_directory(workspace_root, &self.directory)?,
270 &self.file,
271 )
272 }
273
274 fn compile_context(&self, workspace_root: &Path) -> Option<CppCompileContext> {
275 let arguments = match &self.arguments {
276 Some(arguments) if !arguments.is_empty() => arguments.clone(),
277 Some(_) => return None,
278 None => shlex::split(self.command.as_deref()?)?,
279 };
280 parse_compile_arguments(
281 &command_directory(workspace_root, &self.directory)?,
282 &arguments,
283 )
284 }
285}
286
287fn command_directory(workspace_root: &Path, directory: &Path) -> Option<PathBuf> {
288 absolute_path(workspace_root, directory)
289}
290
291fn parse_compile_arguments(directory: &Path, arguments: &[String]) -> Option<CppCompileContext> {
292 if arguments.is_empty() {
293 return None;
294 }
295
296 let driver_basename = driver_basename(&arguments[0]);
297 let mut project_include_roots = Vec::new();
298 let mut system_include_roots = Vec::new();
299 let mut forced_includes = Vec::new();
300 let mut defined_macros = HashSet::default();
301 let mut include_search_roots = Vec::new();
302 let mut explicit_language = None;
303 let mut std_language = None;
304 let mut index = 1;
305 while index < arguments.len() {
306 let argument = &arguments[index];
307 match argument.as_str() {
308 "-x" => {
309 if let Some(language) = language_from_x_value(arguments.get(index + 1)?) {
310 explicit_language = Some(language);
311 }
312 index += 2;
313 }
314 "/TC" => {
315 explicit_language = Some(CompiledLanguage::C);
316 index += 1;
317 }
318 "/TP" => {
319 explicit_language = Some(CompiledLanguage::Cpp);
320 index += 1;
321 }
322 "-I" | "/I" => {
323 let path = argument_path(directory, arguments.get(index + 1)?)?;
324 project_include_roots.push(path.clone());
325 include_search_roots.push(CppIncludeSearchRoot {
326 path,
327 kind: CppIncludeSearchRootKind::Project,
328 });
329 index += 2;
330 }
331 "-iquote" => {
332 let path = argument_path(directory, arguments.get(index + 1)?)?;
333 project_include_roots.push(path.clone());
334 include_search_roots.push(CppIncludeSearchRoot {
335 path,
336 kind: CppIncludeSearchRootKind::Quote,
337 });
338 index += 2;
339 }
340 "-isystem" | "/external:I" | "/imsvc" => {
341 let path = argument_path(directory, arguments.get(index + 1)?)?;
342 system_include_roots.push(path.clone());
343 include_search_roots.push(CppIncludeSearchRoot {
344 path,
345 kind: CppIncludeSearchRootKind::System,
346 });
347 index += 2;
348 }
349 "-include" => {
350 forced_includes.push(argument_path(directory, arguments.get(index + 1)?)?);
351 index += 2;
352 }
353 "-D" => {
354 defined_macros.insert(macro_name(arguments.get(index + 1)?)?);
355 index += 2;
356 }
357 "-U" => {
362 defined_macros.remove(¯o_name(arguments.get(index + 1)?)?);
363 index += 2;
364 }
365 _ => {
366 if let Some(path) = argument
367 .strip_prefix("/external:I")
368 .or_else(|| argument.strip_prefix("/imsvc"))
369 {
370 let path = argument_path(directory, path)?;
371 system_include_roots.push(path.clone());
372 include_search_roots.push(CppIncludeSearchRoot {
373 path,
374 kind: CppIncludeSearchRootKind::System,
375 });
376 } else if let Some(path) = argument
377 .strip_prefix("-I")
378 .or_else(|| argument.strip_prefix("/I"))
379 {
380 let path = argument_path(directory, path)?;
381 project_include_roots.push(path.clone());
382 include_search_roots.push(CppIncludeSearchRoot {
383 path,
384 kind: CppIncludeSearchRootKind::Project,
385 });
386 } else if let Some(path) = argument.strip_prefix("-iquote") {
387 let path = argument_path(directory, path)?;
388 project_include_roots.push(path.clone());
389 include_search_roots.push(CppIncludeSearchRoot {
390 path,
391 kind: CppIncludeSearchRootKind::Quote,
392 });
393 } else if let Some(path) = argument.strip_prefix("-isystem") {
394 let path = argument_path(directory, path)?;
395 system_include_roots.push(path.clone());
396 include_search_roots.push(CppIncludeSearchRoot {
397 path,
398 kind: CppIncludeSearchRootKind::System,
399 });
400 } else if let Some(definition) = argument.strip_prefix("-D") {
401 defined_macros.insert(macro_name(definition)?);
402 } else if let Some(name) = argument.strip_prefix("-U") {
403 defined_macros.remove(¯o_name(name)?);
404 } else if let Some(value) = argument.strip_prefix("-std=") {
405 std_language = Some(if value.contains("++") {
406 CompiledLanguage::Cpp
407 } else {
408 CompiledLanguage::C
409 });
410 } else if let Some(value) = argument.strip_prefix("-x")
411 && let Some(language) = language_from_x_value(value)
412 {
413 explicit_language = Some(language);
414 }
415 index += 1;
416 }
417 }
418 }
419
420 Some(CppCompileContext {
421 project_include_roots,
422 system_include_roots,
423 forced_includes,
424 defined_macros,
425 include_search_roots,
426 driver_basename,
427 explicit_language,
428 std_language,
429 })
430}
431
432fn driver_basename(driver: &str) -> Option<String> {
437 Path::new(driver)
438 .file_stem()
439 .and_then(|stem| stem.to_str())
440 .map(str::to_owned)
441}
442
443fn language_from_x_value(value: &str) -> Option<CompiledLanguage> {
447 let family = value
448 .strip_suffix("-header")
449 .or_else(|| value.strip_suffix("-cpp-output"))
450 .unwrap_or(value);
451 match family {
452 "c" => Some(CompiledLanguage::C),
453 "c++" => Some(CompiledLanguage::Cpp),
454 _ => None,
455 }
456}
457
458fn argument_path(directory: &Path, raw: &str) -> Option<PathBuf> {
459 if raw.is_empty() {
460 return None;
461 }
462 absolute_path(directory, Path::new(raw))
463}
464
465fn absolute_path(directory: &Path, path: &Path) -> Option<PathBuf> {
466 let path = if path.is_absolute() {
467 path.to_path_buf()
468 } else {
469 directory.join(path)
470 }
471 .normalize();
472 path.is_absolute().then_some(path)
473}
474
475fn macro_name(definition: &str) -> Option<String> {
476 let end = definition.find('=').unwrap_or(definition.len());
477 let name = &definition[..end];
478 (!name.is_empty()).then(|| name.to_string())
479}
480
481#[cfg(test)]
482mod tests {
483 use super::{CompiledLanguage, CppCompileContexts, CppExternalIncludeResolution};
484 use brokk_bifrost_core::analyzer::project::TestProject;
485 use brokk_bifrost_core::analyzer::{Language, ProjectFile};
486 use std::path::Path;
487
488 fn context(arguments: &[&str]) -> super::CppCompileContext {
492 let arguments = arguments
493 .iter()
494 .map(|argument| argument.to_string())
495 .collect::<Vec<_>>();
496 super::parse_compile_arguments(Path::new("/workspace"), &arguments)
497 .expect("non-empty argument vector parses")
498 }
499
500 fn project_with_database(database: Option<&str>) -> (tempfile::TempDir, TestProject) {
501 let temp = tempfile::tempdir().expect("temp dir");
502 let root = temp.path().canonicalize().expect("canonical root");
503 ProjectFile::new(root.clone(), "src/main.cpp")
504 .write("int main() { return 0; }")
505 .expect("source");
506 if let Some(database) = database {
507 ProjectFile::new(root.clone(), "compile_commands.json")
508 .write(database)
509 .expect("database");
510 }
511 (temp, TestProject::new(root, Language::Cpp))
512 }
513
514 #[test]
515 fn missing_or_malformed_database_has_no_context() {
516 let (_temp, project) = project_with_database(None);
517 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
518 assert!(
519 CppCompileContexts::load(&project)
520 .contexts_for(&file)
521 .is_empty()
522 );
523
524 let (_temp, project) = project_with_database(Some("not json"));
525 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
526 assert!(
527 CppCompileContexts::load(&project)
528 .contexts_for(&file)
529 .is_empty()
530 );
531 }
532
533 #[test]
534 fn arguments_entry_collects_include_paths_and_macro_names() {
535 let (_temp, project) = project_with_database(Some(
536 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-iquotequotes","-isystem","system-include","-DDEBUG=1","-D","FEATURE","-c","src/main.cpp"]}]"#,
537 ));
538 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
539 let contexts = CppCompileContexts::load(&project);
540 let [context] = contexts.contexts_for(&file) else {
541 panic!("one matching context");
542 };
543
544 assert_eq!(
545 vec![
546 project.root_path().join("include"),
547 project.root_path().join("quotes"),
548 ],
549 context.project_include_roots
550 );
551 assert_eq!(
552 vec![project.root_path().join("system-include")],
553 context.system_include_roots
554 );
555 assert!(context.defined_macros.contains("DEBUG"));
556 assert!(context.defined_macros.contains("FEATURE"));
557 }
558
559 #[test]
560 fn quoted_command_entry_is_tokenized_without_executing_it() {
561 let (_temp, project) = project_with_database(Some(
562 r#"[{"directory":".","file":"src/main.cpp","command":"clang++ -I 'project include' -DNAME=\\\"two words\\\" -c src/main.cpp"}]"#,
563 ));
564 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
565 let contexts = CppCompileContexts::load(&project);
566 let [context] = contexts.contexts_for(&file) else {
567 panic!("one matching context");
568 };
569
570 assert_eq!(
571 vec![project.root_path().join("project include")],
572 context.project_include_roots
573 );
574 assert!(context.defined_macros.contains("NAME"));
575 }
576
577 #[test]
578 fn undefine_flags_apply_in_command_order() {
579 let context = context(&[
580 "cc",
581 "-DKEPT",
582 "-DCANCELLED",
583 "-U",
584 "CANCELLED",
585 "-UREVIVED",
586 "-DREVIVED",
587 "-UNEVER_DEFINED",
588 ]);
589 assert!(context.defined_macros.contains("KEPT"));
590 assert!(context.defined_macros.contains("REVIVED"));
591 assert!(!context.defined_macros.contains("CANCELLED"));
592 assert!(!context.defined_macros.contains("NEVER_DEFINED"));
593 }
594
595 #[test]
596 fn a_file_compiled_in_two_configurations_keeps_both() {
597 let (_temp, project) = project_with_database(Some(
598 r#"[
599 {"directory":".","file":"src/main.cpp","arguments":["clang++","-c","src/main.cpp"]},
600 {"directory":".","file":"src/main.cpp","arguments":["clang++","-DOTHER","-c","src/main.cpp"]},
601 {"directory":".","file":"src/other.cpp","arguments":["clang++","-c","src/other.cpp"]}
602 ]"#,
603 ));
604 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
605 let contexts = CppCompileContexts::load(&project);
606 let candidates = contexts.contexts_for(&file);
607
608 assert_eq!(2, candidates.len());
611 assert!(candidates[0].defined_macros.is_empty());
612 assert!(candidates[1].defined_macros.contains("OTHER"));
613 }
614
615 #[test]
616 fn repeated_identical_entries_are_one_configuration() {
617 let (_temp, project) = project_with_database(Some(
618 r#"[
619 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]},
620 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]}
621 ]"#,
622 ));
623 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
624 let contexts = CppCompileContexts::load(&project);
625
626 assert_eq!(1, contexts.contexts_for(&file).len());
629 }
630
631 #[test]
632 fn an_unmatched_file_has_no_context() {
633 let (_temp, project) = project_with_database(Some(
634 r#"[{"directory":".","file":"src/other.cpp","arguments":["clang++","-c","src/other.cpp"]}]"#,
635 ));
636 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
637 assert!(
638 CppCompileContexts::load(&project)
639 .contexts_for(&file)
640 .is_empty()
641 );
642 }
643
644 #[test]
645 fn an_explicit_system_root_declares_an_angle_include() {
646 let (_temp, project) = project_with_database(Some(
647 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-isystem","fake-system/include","-c","src/main.cpp"]}]"#,
648 ));
649 let header = ProjectFile::new(
650 project.root_path().to_path_buf(),
651 "fake-system/include/vector",
652 );
653 header
654 .write("namespace std { class vector {}; }")
655 .expect("header");
656 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
657
658 assert_eq!(
659 CppExternalIncludeResolution::Declared {
660 root: project
661 .root_path()
662 .join("fake-system/include")
663 .canonicalize()
664 .expect("canonical root"),
665 header: header.abs_path().canonicalize().expect("canonical header"),
666 },
667 CppCompileContexts::load(&project)
668 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
669 );
670 }
671
672 #[test]
673 fn an_external_project_root_declares_but_a_workspace_project_root_does_not() {
674 let external = tempfile::tempdir().expect("external root");
675 let external_root = external
676 .path()
677 .canonicalize()
678 .expect("canonical external root");
679 std::fs::write(external_root.join("vendor.hpp"), "class Vendor {};")
680 .expect("external header");
681 let database = format!(
682 r#"[{{"directory":".","file":"src/main.cpp","arguments":["clang++","-I","{}","-I","include","-c","src/main.cpp"]}}]"#,
683 external_root.display()
684 );
685 let (_temp, project) = project_with_database(Some(&database));
686 ProjectFile::new(project.root_path().to_path_buf(), "include/local.hpp")
687 .write("class Local {};")
688 .expect("local header");
689 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
690 let contexts = CppCompileContexts::load(&project);
691
692 assert!(matches!(
693 contexts.resolve_external_angle_include(&file, std::path::Path::new("vendor.hpp")),
694 CppExternalIncludeResolution::Declared { .. }
695 ));
696 assert_eq!(
697 CppExternalIncludeResolution::Undeclared,
698 contexts.resolve_external_angle_include(&file, std::path::Path::new("local.hpp"))
699 );
700 }
701
702 #[test]
703 fn configurations_must_agree_on_the_external_header() {
704 let first = tempfile::tempdir().expect("first root");
705 let second = tempfile::tempdir().expect("second root");
706 let first = first.path().canonicalize().expect("canonical first root");
707 let second = second.path().canonicalize().expect("canonical second root");
708 std::fs::write(first.join("vector"), "namespace std { class vector {}; }")
709 .expect("first header");
710 std::fs::write(second.join("vector"), "namespace std { class vector {}; }")
711 .expect("second header");
712 let database = format!(
713 r#"[
714 {{"directory":".","file":"src/main.cpp","arguments":["clang++","-isystem","{}","-c","src/main.cpp"]}},
715 {{"directory":".","file":"src/main.cpp","arguments":["clang++","-isystem","{}","-c","src/main.cpp"]}}
716 ]"#,
717 first.display(),
718 second.display()
719 );
720 let (_temp, project) = project_with_database(Some(&database));
721 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
722
723 assert_eq!(
724 CppExternalIncludeResolution::Conflicting,
725 CppCompileContexts::load(&project)
726 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
727 );
728 }
729
730 #[test]
731 fn external_include_cannot_escape_its_declared_root() {
732 let external = tempfile::tempdir().expect("external root");
733 let root = external.path().canonicalize().expect("canonical root");
734 std::fs::create_dir_all(root.join("include")).expect("include directory");
735 std::fs::write(root.join("outside.hpp"), "class Outside {};").expect("outside header");
736 let database = format!(
737 r#"[{{"directory":".","file":"src/main.cpp","arguments":["clang++","-isystem","{}","-c","src/main.cpp"]}}]"#,
738 root.join("include").display()
739 );
740 let (_temp, project) = project_with_database(Some(&database));
741 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
742
743 assert_eq!(
744 CppExternalIncludeResolution::Undeclared,
745 CppCompileContexts::load(&project)
746 .resolve_external_angle_include(&file, std::path::Path::new("../outside.hpp"))
747 );
748 assert_eq!(
749 CppExternalIncludeResolution::Undeclared,
750 CppCompileContexts::load(&project)
751 .resolve_external_angle_include(&file, &root.join("outside.hpp"))
752 );
753 }
754
755 #[test]
756 fn msvc_project_and_system_include_flags_preserve_search_order() {
757 let (_temp, project) = project_with_database(Some(
758 r#"[{"directory":".","file":"src/main.cpp","arguments":["cl.exe","/I","vendor/include","/external:Ifake-system/include","/imsvc","toolchain/include","/c","src/main.cpp"]}]"#,
759 ));
760 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
761 let contexts = CppCompileContexts::load(&project);
762 let [context] = contexts.contexts_for(&file) else {
763 panic!("one MSVC compile context");
764 };
765
766 assert_eq!(1, context.project_include_roots.len());
767 assert_eq!(2, context.system_include_roots.len());
768 }
769
770 #[test]
771 fn default_by_extension_c_is_c_and_everything_else_is_cpp() {
772 let cx = context(&["cc", "-c", "file.c"]);
773 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
774
775 let cx = context(&["cc", "-c", "file.cc"]);
776 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
777 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.hpp")));
778 }
779
780 #[test]
781 fn driver_basename_ending_in_plus_plus_selects_cpp_over_a_c_extension() {
782 let cx = context(&["clang++", "-c", "file.c"]);
783 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
784
785 let cx = context(&["arm-none-eabi-gcc", "-c", "file.c"]);
788 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
789
790 let cx = context(&["arm-none-eabi-g++", "-c", "file.h"]);
792 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.h")));
793 }
794
795 #[test]
796 fn driver_exe_suffix_is_stripped_before_the_plus_plus_check() {
797 let cx = context(&["clang++.exe", "-c", "file.c"]);
798 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
799 }
800
801 #[test]
802 fn std_family_outranks_the_driver_basename() {
803 let cx = context(&["g++", "-std=gnu11", "-c", "file.c"]);
806 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
807
808 let cx = context(&["gcc", "-std=c++17", "-c", "file.cc"]);
809 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
810
811 let cx = context(&["gcc", "-std=c17", "-c", "file.cc"]);
812 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cc")));
813
814 let cx = context(&["gcc", "-std=gnu++20", "-c", "file.c"]);
815 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
816 }
817
818 #[test]
819 fn explicit_x_split_form_outranks_std_and_driver() {
820 let cx = context(&["clang", "-x", "c", "-std=c++17", "-c", "file.cpp"]);
821 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
822 }
823
824 #[test]
825 fn explicit_x_glued_form_is_recognized() {
826 let cx = context(&["gcc", "-xc++", "-c", "file.c"]);
827 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
828
829 let cx = context(&["g++", "-xc", "-c", "file.cpp"]);
830 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
831 }
832
833 #[test]
834 fn explicit_x_on_a_c_file_overrides_the_extension() {
835 let cx = context(&["gcc", "-x", "c++", "-c", "file.c"]);
838 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
839 }
840
841 #[test]
842 fn unrecognized_x_value_is_ignored() {
843 let cx = context(&["gcc", "-x", "assembler", "-c", "file.s"]);
844 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.s")));
847 }
848
849 #[test]
850 fn msvc_tc_and_tp_force_the_language_regardless_of_extension() {
851 let cx = context(&["cl.exe", "/TC", "/c", "file.cpp"]);
852 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
853
854 let cx = context(&["cl.exe", "/TP", "/c", "file.c"]);
855 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
856 }
857
858 #[test]
859 fn cl_exe_without_tc_or_tp_decides_by_extension() {
860 let cx = context(&["cl.exe", "/c", "file.c"]);
861 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
862
863 let cx = context(&["cl.exe", "/c", "file.cpp"]);
864 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cpp")));
865 }
866}