1#![allow(clippy::missing_errors_doc)]
7
8pub mod arduino;
9pub mod parse_archiver;
10pub mod parse_linker;
11pub mod parse_rustfmt;
12pub mod response_file;
13pub mod strict_paths;
14
15use std::sync::Arc;
16use zccache_core::NormalizedPath;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum CompilerFamily {
21 Gcc,
23 Clang,
25 Msvc,
27 Rustc,
29 Rustfmt,
31}
32
33impl CompilerFamily {
34 #[must_use]
37 pub fn supports_depfile(&self) -> bool {
38 matches!(self, CompilerFamily::Gcc | CompilerFamily::Clang)
39 }
40
41 #[must_use]
44 pub fn pch_extension(&self) -> Option<&'static str> {
45 match self {
46 CompilerFamily::Gcc => Some("gch"),
47 CompilerFamily::Clang => Some("pch"),
48 CompilerFamily::Msvc | CompilerFamily::Rustc | CompilerFamily::Rustfmt => None,
49 }
50 }
51
52 #[must_use]
54 pub fn is_formatter(&self) -> bool {
55 matches!(self, CompilerFamily::Rustfmt)
56 }
57}
58
59#[derive(Debug, Clone)]
61pub enum ParsedInvocation {
62 Cacheable(CacheableCompilation),
64 MultiFile {
66 compilations: Vec<CacheableCompilation>,
68 original_args: Arc<[String]>,
70 source_indices: Vec<usize>,
73 },
74 NonCacheable {
76 reason: String,
78 },
79}
80
81#[derive(Debug, Clone)]
83pub struct CacheableCompilation {
84 pub compiler: NormalizedPath,
86 pub family: CompilerFamily,
88 pub source_file: NormalizedPath,
90 pub output_file: NormalizedPath,
92 pub original_args: Arc<[String]>,
94 pub unknown_flags: Vec<String>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub(crate) enum SourceMode {
103 Normal,
105 Header,
107 HeaderUnit,
109 Module,
111}
112
113impl SourceMode {
114 pub(crate) fn implies_compilation(self) -> bool {
118 matches!(self, SourceMode::Header | SourceMode::HeaderUnit)
119 }
120}
121
122fn source_mode_from_language(lang: &str) -> Option<SourceMode> {
125 match lang {
126 "c-header" | "c++-header" => Some(SourceMode::Header),
127 "c-header-unit" | "c++-header-unit" => Some(SourceMode::HeaderUnit),
128 "c++-module" => Some(SourceMode::Module),
129 _ => None,
130 }
131}
132
133const SOURCE_EXTENSIONS: &[&str] = &[
135 "c", "cc", "cpp", "cxx", "c++", "C", "m", "mm", "i", "ii", "cppm", "ixx",
136];
137
138const MODULE_EXTENSIONS: &[&str] = &["cppm", "ixx"];
140
141fn source_mode_from_extension(path: &str) -> SourceMode {
143 if let Some(ext) = std::path::Path::new(path)
144 .extension()
145 .and_then(|e| e.to_str())
146 {
147 if MODULE_EXTENSIONS.contains(&ext) {
148 return SourceMode::Module;
149 }
150 }
151 SourceMode::Normal
152}
153
154#[must_use]
156pub fn detect_family(compiler: &str) -> CompilerFamily {
157 let basename = compiler.rsplit(['/', '\\']).next().unwrap_or(compiler);
159 let name = match basename.rsplit_once('.') {
160 Some((stem, _)) => stem,
161 None => basename,
162 };
163 if name == "rustfmt" || name.starts_with("rustfmt-") {
164 CompilerFamily::Rustfmt
165 } else if name == "rustc"
166 || name.starts_with("rustc-")
167 || name == "clippy-driver"
168 || name.starts_with("clippy-driver-")
169 {
170 CompilerFamily::Rustc
171 } else if name.contains("clang") || name == "emcc" || name == "em++" {
172 CompilerFamily::Clang
173 } else if name.eq_ignore_ascii_case("cl") {
174 CompilerFamily::Msvc
175 } else {
176 CompilerFamily::Gcc
177 }
178}
179
180fn is_source_file(path: &str) -> bool {
182 if let Some(ext) = std::path::Path::new(path)
183 .extension()
184 .and_then(|e| e.to_str())
185 {
186 SOURCE_EXTENSIONS.contains(&ext)
187 } else {
188 false
189 }
190}
191
192fn default_output(
208 source: &str,
209 family: CompilerFamily,
210 mode: SourceMode,
211 has_precompile: bool,
212) -> String {
213 match mode {
214 SourceMode::Header => {
215 if let Some(ext) = family.pch_extension() {
217 let filename = std::path::Path::new(source)
218 .file_name()
219 .and_then(|f| f.to_str())
220 .unwrap_or(source);
221 return format!("{filename}.{ext}");
222 }
223 }
224 SourceMode::HeaderUnit => {
225 let filename = std::path::Path::new(source)
227 .file_name()
228 .and_then(|f| f.to_str())
229 .unwrap_or(source);
230 return format!("{filename}.pcm");
231 }
232 SourceMode::Module | SourceMode::Normal => {
233 if has_precompile {
234 let stem = std::path::Path::new(source)
236 .file_stem()
237 .and_then(|s| s.to_str())
238 .unwrap_or("a");
239 return format!("{stem}.pcm");
240 }
241 }
242 }
243 let stem = std::path::Path::new(source)
245 .file_stem()
246 .and_then(|s| s.to_str())
247 .unwrap_or("a");
248 format!("{stem}.o")
249}
250
251const FLAGS_WITH_VALUE: &[&str] = &[
253 "-o",
254 "-D",
255 "-U",
256 "-I",
257 "-isystem",
258 "-iquote",
259 "-idirafter",
260 "-include",
261 "-include-pch",
262 "-isysroot",
263 "-target",
264 "--target",
265 "-MF",
266 "-MQ",
267 "-MT",
268 "-std",
269 "-x",
270 "-arch",
271 "-Xclang",
272 "-mllvm",
273 "--serialize-diagnostics",
274];
275
276#[must_use]
284pub fn parse_invocation(compiler: &str, args: &[String]) -> ParsedInvocation {
285 let family = detect_family(compiler);
286 if family == CompilerFamily::Rustfmt {
288 return ParsedInvocation::NonCacheable {
289 reason: "rustfmt is handled via the format cache path, not compile cache".to_string(),
290 };
291 }
292 if family == CompilerFamily::Rustc {
294 return parse_rustc_invocation(compiler, args);
295 }
296
297 let mut has_c_flag = false;
298 let mut has_precompile_flag = false;
299 let mut source_files: Vec<(String, usize, SourceMode)> = Vec::new();
300 let mut output_file: Option<String> = None;
301 let mut current_mode = SourceMode::Normal;
302 let mut unknown_flags: Vec<String> = Vec::new();
303
304 let mut i = 0;
305 while i < args.len() {
306 let arg = &args[i];
307
308 if arg == "-E" || arg == "-M" || arg == "-MM" {
310 return ParsedInvocation::NonCacheable {
311 reason: format!("preprocessing-only flag: {arg}"),
312 };
313 }
314
315 if arg == "-" {
316 return ParsedInvocation::NonCacheable {
317 reason: "stdin source not cacheable".to_string(),
318 };
319 }
320
321 if arg == "-c" {
322 has_c_flag = true;
323 i += 1;
324 continue;
325 }
326
327 if arg == "--precompile" {
330 has_precompile_flag = true;
331 i += 1;
332 continue;
333 }
334
335 if arg == "-o" {
337 if let Some(next) = args.get(i + 1) {
338 output_file = Some(next.clone());
339 i += 2;
340 } else {
341 i += 1;
342 }
343 continue;
344 } else if let Some(path) = arg.strip_prefix("-o") {
345 output_file = Some(path.to_string());
346 i += 1;
347 continue;
348 }
349
350 if let Some(&flag) = FLAGS_WITH_VALUE.iter().find(|&&f| f == arg.as_str()) {
352 if flag == "-x" && i + 1 < args.len() {
353 current_mode =
354 source_mode_from_language(&args[i + 1]).unwrap_or(SourceMode::Normal);
355 }
356 i += 2;
357 continue;
358 }
359
360 if arg.starts_with('-') {
362 unknown_flags.push(arg.clone());
363 i += 1;
364 continue;
365 }
366
367 let effective_mode = if current_mode != SourceMode::Normal {
372 current_mode
373 } else {
374 source_mode_from_extension(arg)
375 };
376 if is_source_file(arg) || current_mode != SourceMode::Normal {
377 source_files.push((arg.clone(), i, effective_mode));
378 }
379
380 i += 1;
381 }
382
383 if !has_c_flag && !has_precompile_flag && !current_mode.implies_compilation() {
387 return ParsedInvocation::NonCacheable {
388 reason: "no -c flag (likely a link invocation)".to_string(),
389 };
390 }
391
392 if source_files.is_empty() {
393 return ParsedInvocation::NonCacheable {
394 reason: "no source file found".to_string(),
395 };
396 }
397
398 let family = detect_family(compiler);
399
400 if source_files.len() > 1 {
403 let source_indices: Vec<usize> = source_files.iter().map(|(_, idx, _)| *idx).collect();
404 let shared_args: Arc<[String]> = Arc::from(args.to_vec());
405 let compilations = source_files
406 .iter()
407 .map(|(src, _, mode)| CacheableCompilation {
408 compiler: NormalizedPath::new(compiler),
409 family,
410 source_file: NormalizedPath::new(src),
411 output_file: NormalizedPath::new(default_output(
412 src,
413 family,
414 *mode,
415 has_precompile_flag,
416 )),
417 original_args: Arc::clone(&shared_args),
418 unknown_flags: unknown_flags.clone(),
419 })
420 .collect();
421 return ParsedInvocation::MultiFile {
422 compilations,
423 original_args: shared_args,
424 source_indices,
425 };
426 }
427
428 let (source, _, mode) = source_files.into_iter().next().unwrap();
430 let output =
431 output_file.unwrap_or_else(|| default_output(&source, family, mode, has_precompile_flag));
432
433 ParsedInvocation::Cacheable(CacheableCompilation {
434 compiler: NormalizedPath::new(compiler),
435 family,
436 source_file: NormalizedPath::new(source),
437 output_file: NormalizedPath::new(output),
438 original_args: Arc::from(args.to_vec()),
439 unknown_flags,
440 })
441}
442
443const RUSTC_CACHEABLE_CRATE_TYPES: &[&str] = &["lib", "rlib", "staticlib"];
445
446const RUSTC_FLAGS_WITH_VALUE: &[&str] = &[
448 "--edition",
449 "--crate-type",
450 "--crate-name",
451 "--emit",
452 "--out-dir",
453 "--target",
454 "--cap-lints",
455 "--extern",
456 "--error-format",
457 "--json",
458 "--color",
459 "--diagnostic-width",
460 "--sysroot",
461 "--cfg",
462 "--check-cfg",
463 "-o",
464 "-L",
465 "-C",
466 "-A",
467 "-W",
468 "-D",
469 "-F",
470 "--codegen",
471 "--remap-path-prefix",
472 "--env-set",
473];
474
475fn parse_rustc_invocation(compiler: &str, args: &[String]) -> ParsedInvocation {
480 let mut crate_types: Vec<String> = Vec::new();
481 let mut source_file: Option<String> = None;
482 let mut output_file: Option<String> = None;
483 let mut out_dir: Option<String> = None;
484 let mut crate_name: Option<String> = None;
485 let mut extra_filename: Option<String> = None;
486 let mut emit_types: Vec<String> = Vec::new();
487 let mut unknown_flags: Vec<String> = Vec::new();
488
489 let mut i = 0;
490 while i < args.len() {
491 let arg = &args[i];
492
493 if arg == "--crate-type" {
496 if let Some(next) = args.get(i + 1) {
497 crate_types.extend(next.split(',').map(|s| s.to_string()));
498 i += 2;
499 continue;
500 }
501 } else if let Some(val) = arg.strip_prefix("--crate-type=") {
502 crate_types.extend(val.split(',').map(|s| s.to_string()));
503 i += 1;
504 continue;
505 }
506
507 if arg == "--crate-name" {
509 if let Some(next) = args.get(i + 1) {
510 crate_name = Some(next.clone());
511 i += 2;
512 continue;
513 }
514 } else if let Some(val) = arg.strip_prefix("--crate-name=") {
515 crate_name = Some(val.to_string());
516 i += 1;
517 continue;
518 }
519
520 if arg == "--emit" {
522 if let Some(next) = args.get(i + 1) {
523 emit_types.extend(next.split(',').map(|s| {
524 s.split('=').next().unwrap_or(s).to_string()
526 }));
527 i += 2;
528 continue;
529 }
530 } else if let Some(val) = arg.strip_prefix("--emit=") {
531 emit_types.extend(
532 val.split(',')
533 .map(|s| s.split('=').next().unwrap_or(s).to_string()),
534 );
535 i += 1;
536 continue;
537 }
538
539 if arg == "--out-dir" {
541 if let Some(next) = args.get(i + 1) {
542 out_dir = Some(next.clone());
543 i += 2;
544 continue;
545 }
546 } else if let Some(val) = arg.strip_prefix("--out-dir=") {
547 out_dir = Some(val.to_string());
548 i += 1;
549 continue;
550 }
551
552 if arg == "-o" {
554 if let Some(next) = args.get(i + 1) {
555 output_file = Some(next.clone());
556 i += 2;
557 continue;
558 }
559 }
560
561 if arg == "-C" || arg == "--codegen" {
563 if let Some(next) = args.get(i + 1) {
564 if let Some(val) = next.strip_prefix("extra-filename=") {
565 extra_filename = Some(val.to_string());
566 }
567 i += 2;
568 continue;
569 }
570 } else if let Some(rest) = arg.strip_prefix("-C") {
571 if !rest.is_empty() {
572 if let Some(val) = rest.strip_prefix("extra-filename=") {
573 extra_filename = Some(val.to_string());
574 }
575 i += 1;
576 continue;
577 }
578 }
579
580 if let Some(&_flag) = RUSTC_FLAGS_WITH_VALUE.iter().find(|&&f| f == arg.as_str()) {
582 i += 2;
583 continue;
584 }
585
586 if arg.starts_with("--") && arg.contains('=') {
588 i += 1;
589 continue;
590 }
591
592 if arg.starts_with('-') {
594 unknown_flags.push(arg.clone());
595 i += 1;
596 continue;
597 }
598
599 if arg.ends_with(".rs") {
601 source_file = Some(arg.clone());
602 }
603
604 i += 1;
605 }
606
607 let source = match source_file {
609 Some(s) => s,
610 None => {
611 return ParsedInvocation::NonCacheable {
612 reason: "no .rs source file found".to_string(),
613 };
614 }
615 };
616
617 if crate_types.is_empty() {
624 crate_types.push("bin".to_string());
625 }
626
627 for ct in &crate_types {
629 if !RUSTC_CACHEABLE_CRATE_TYPES.contains(&ct.as_str()) {
630 return ParsedInvocation::NonCacheable {
631 reason: format!("non-cacheable crate type: {ct}"),
632 };
633 }
634 }
635
636 let has_link_emit = emit_types.iter().any(|t| t == "link");
640 let primary_ext = if !has_link_emit && emit_types.iter().any(|t| t == "metadata") {
641 "rmeta"
642 } else {
643 match crate_types.first().map(|s| s.as_str()) {
644 Some("staticlib") => "a",
645 _ => "rlib",
646 }
647 };
648
649 let output = if let Some(o) = output_file {
651 o
652 } else if let Some(ref dir) = out_dir {
653 let name = crate_name.as_deref().unwrap_or("unknown");
654 let suffix = extra_filename.as_deref().unwrap_or("");
655 NormalizedPath::new(dir)
657 .join(format!("lib{name}{suffix}.{primary_ext}"))
658 .to_string_lossy()
659 .into_owned()
660 } else {
661 let name = crate_name.as_deref().unwrap_or_else(|| {
662 std::path::Path::new(&source)
663 .file_stem()
664 .and_then(|s| s.to_str())
665 .unwrap_or("unknown")
666 });
667 format!("lib{name}.{primary_ext}")
668 };
669
670 ParsedInvocation::Cacheable(CacheableCompilation {
671 compiler: NormalizedPath::new(compiler),
672 family: CompilerFamily::Rustc,
673 source_file: NormalizedPath::new(source),
674 output_file: NormalizedPath::new(output),
675 original_args: Arc::from(args.to_vec()),
676 unknown_flags,
677 })
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 fn args(s: &[&str]) -> Vec<String> {
685 s.iter().map(|x| x.to_string()).collect()
686 }
687
688 #[test]
689 fn basic_cacheable_compilation() {
690 let result = parse_invocation("clang++", &args(&["-c", "hello.cpp", "-o", "hello.o"]));
691 match result {
692 ParsedInvocation::Cacheable(c) => {
693 assert_eq!(c.source_file, NormalizedPath::new("hello.cpp"));
694 assert_eq!(c.output_file, NormalizedPath::new("hello.o"));
695 assert_eq!(c.family, CompilerFamily::Clang);
696 }
697 other => panic!("expected cacheable, got: {other:?}"),
698 }
699 }
700
701 #[test]
702 fn no_c_flag_is_non_cacheable() {
703 let result = parse_invocation("gcc", &args(&["hello.cpp", "-o", "hello"]));
704 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
705 }
706
707 #[test]
708 fn preprocessing_only_non_cacheable() {
709 let result = parse_invocation("gcc", &args(&["-E", "hello.cpp"]));
710 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
711 }
712
713 #[test]
714 fn multi_file_split() {
715 let result = parse_invocation("gcc", &args(&["-c", "a.cpp", "b.cpp"]));
716 match result {
717 ParsedInvocation::MultiFile {
718 compilations,
719 source_indices,
720 ..
721 } => {
722 assert_eq!(compilations.len(), 2);
723 assert_eq!(compilations[0].source_file, NormalizedPath::new("a.cpp"));
724 assert_eq!(compilations[0].output_file, NormalizedPath::new("a.o"));
725 assert_eq!(compilations[1].source_file, NormalizedPath::new("b.cpp"));
726 assert_eq!(compilations[1].output_file, NormalizedPath::new("b.o"));
727 assert_eq!(source_indices, vec![1, 2]);
728 }
729 other => panic!("expected MultiFile, got: {other:?}"),
730 }
731 }
732
733 #[test]
734 fn multi_file_with_flags() {
735 let result = parse_invocation(
736 "g++",
737 &args(&["-c", "-O2", "main.cpp", "-Wall", "util.cpp"]),
738 );
739 match result {
740 ParsedInvocation::MultiFile {
741 compilations,
742 original_args,
743 source_indices,
744 } => {
745 assert_eq!(compilations.len(), 2);
746 assert_eq!(compilations[0].source_file, NormalizedPath::new("main.cpp"));
747 assert_eq!(compilations[1].source_file, NormalizedPath::new("util.cpp"));
748 assert!(original_args.contains(&"-O2".to_string()));
750 assert!(original_args.contains(&"-Wall".to_string()));
751 assert_eq!(source_indices, vec![2, 4]);
752 }
753 other => panic!("expected MultiFile, got: {other:?}"),
754 }
755 }
756
757 #[test]
758 fn multi_file_mixed_extensions() {
759 let result = parse_invocation("gcc", &args(&["-c", "file1.c", "file2.cpp"]));
760 match result {
761 ParsedInvocation::MultiFile { compilations, .. } => {
762 assert_eq!(compilations.len(), 2);
763 assert_eq!(compilations[0].source_file, NormalizedPath::new("file1.c"));
764 assert_eq!(
765 compilations[1].source_file,
766 NormalizedPath::new("file2.cpp")
767 );
768 }
769 other => panic!("expected MultiFile, got: {other:?}"),
770 }
771 }
772
773 #[test]
774 fn stdin_non_cacheable() {
775 let result = parse_invocation("gcc", &args(&["-c", "-"]));
776 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
777 }
778
779 #[test]
780 fn default_output_name() {
781 let result = parse_invocation("gcc", &args(&["-c", "foo.cpp"]));
782 match result {
783 ParsedInvocation::Cacheable(c) => {
784 assert_eq!(c.output_file, NormalizedPath::new("foo.o"));
785 }
786 _ => panic!("expected cacheable"),
787 }
788 }
789
790 #[test]
791 fn original_args_preserved() {
792 let input = args(&["-c", "hello.cpp", "-O2", "-std=c++17", "-DNDEBUG", "-Wall"]);
793 let result = parse_invocation("clang++", &input);
794 match result {
795 ParsedInvocation::Cacheable(c) => {
796 assert_eq!(*c.original_args, *input);
797 }
798 _ => panic!("expected cacheable"),
799 }
800 }
801
802 #[test]
803 fn unknown_flags_preserved_in_original_args() {
804 let input = args(&[
805 "-c",
806 "hello.cpp",
807 "--deploy-dependencies",
808 "--custom-flag=value",
809 "-o",
810 "hello.o",
811 ]);
812 let result = parse_invocation("clang++", &input);
813 match result {
814 ParsedInvocation::Cacheable(c) => {
815 assert_eq!(*c.original_args, *input);
816 assert_eq!(c.source_file, NormalizedPath::new("hello.cpp"));
817 assert_eq!(c.output_file, NormalizedPath::new("hello.o"));
818 }
819 other => panic!("expected cacheable, got: {other:?}"),
820 }
821 }
822
823 #[test]
824 fn include_pch_flag_with_value() {
825 let result = parse_invocation(
826 "clang++",
827 &args(&["-c", "foo.cpp", "-include-pch", "pch.h.pch", "-o", "foo.o"]),
828 );
829 match result {
830 ParsedInvocation::Cacheable(c) => {
831 assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
833 assert!(c.original_args.contains(&"-include-pch".to_string()));
835 assert!(c.original_args.contains(&"pch.h.pch".to_string()));
836 }
837 other => panic!("expected cacheable, got: {other:?}"),
838 }
839 }
840
841 #[test]
842 fn pch_generation_cpp_header_is_cacheable() {
843 let result = parse_invocation(
845 "clang++",
846 &args(&["-x", "c++-header", "-c", "pch.h", "-o", "pch.h.pch"]),
847 );
848 match result {
849 ParsedInvocation::Cacheable(c) => {
850 assert_eq!(c.source_file, NormalizedPath::new("pch.h"));
851 assert_eq!(c.output_file, NormalizedPath::new("pch.h.pch"));
852 }
853 other => panic!("expected cacheable, got: {other:?}"),
854 }
855 }
856
857 #[test]
858 fn pch_generation_c_header_is_cacheable() {
859 let result = parse_invocation(
861 "gcc",
862 &args(&["-x", "c-header", "-c", "stdafx.h", "-o", "stdafx.h.gch"]),
863 );
864 match result {
865 ParsedInvocation::Cacheable(c) => {
866 assert_eq!(c.source_file, NormalizedPath::new("stdafx.h"));
867 assert_eq!(c.output_file, NormalizedPath::new("stdafx.h.gch"));
868 }
869 other => panic!("expected cacheable, got: {other:?}"),
870 }
871 }
872
873 #[test]
874 fn pch_generation_without_c_flag_is_cacheable() {
875 let result = parse_invocation(
879 "clang++",
880 &args(&["-x", "c++-header", "FastLED.h", "-o", "FastLED.h.pch"]),
881 );
882 match result {
883 ParsedInvocation::Cacheable(c) => {
884 assert_eq!(c.source_file, NormalizedPath::new("FastLED.h"));
885 assert_eq!(c.output_file, NormalizedPath::new("FastLED.h.pch"));
886 }
887 other => panic!("expected cacheable, got: {other:?}"),
888 }
889 }
890
891 #[test]
892 fn pch_generation_c_header_without_c_flag_is_cacheable() {
893 let result = parse_invocation(
894 "gcc",
895 &args(&["-x", "c-header", "stdafx.h", "-o", "stdafx.h.gch"]),
896 );
897 match result {
898 ParsedInvocation::Cacheable(c) => {
899 assert_eq!(c.source_file, NormalizedPath::new("stdafx.h"));
900 assert_eq!(c.output_file, NormalizedPath::new("stdafx.h.gch"));
901 }
902 other => panic!("expected cacheable, got: {other:?}"),
903 }
904 }
905
906 #[test]
907 fn pch_generation_with_meson_flags_is_cacheable() {
908 let result = parse_invocation(
910 "ctc-clang++",
911 &args(&[
912 "-x",
913 "c++-header",
914 "FastLED.h",
915 "-o",
916 "FastLED.h.pch",
917 "-MD",
918 "-MF",
919 "FastLED.h.pch.d",
920 "-fPIC",
921 "-Iinclude",
922 "-Werror=invalid-pch",
923 ]),
924 );
925 match result {
926 ParsedInvocation::Cacheable(c) => {
927 assert_eq!(c.source_file, NormalizedPath::new("FastLED.h"));
928 assert_eq!(c.output_file, NormalizedPath::new("FastLED.h.pch"));
929 }
930 other => panic!("expected cacheable, got: {other:?}"),
931 }
932 }
933
934 #[test]
935 fn header_without_x_flag_is_not_source() {
936 let result = parse_invocation("clang++", &args(&["-c", "pch.h"]));
938 assert!(
939 matches!(result, ParsedInvocation::NonCacheable { .. }),
940 "bare .h without -x header mode should be non-cacheable"
941 );
942 }
943
944 #[test]
945 fn x_flag_reset_disables_header_mode() {
946 let result = parse_invocation(
950 "clang++",
951 &args(&[
952 "-x",
953 "c++-header",
954 "pch.h",
955 "-x",
956 "c++",
957 "main.cpp",
958 "-c",
959 "-o",
960 "main.o",
961 ]),
962 );
963 match result {
964 ParsedInvocation::MultiFile { compilations, .. } => {
965 assert_eq!(compilations.len(), 2);
966 assert_eq!(compilations[0].source_file, NormalizedPath::new("pch.h"));
967 assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
968 }
969 other => panic!("expected MultiFile, got: {other:?}"),
970 }
971 }
972
973 #[test]
974 fn x_cpp_after_header_is_normal_compilation() {
975 let result = parse_invocation(
979 "clang++",
980 &args(&[
981 "-x",
982 "c++-header",
983 "-x",
984 "c++",
985 "main.cpp",
986 "-c",
987 "-o",
988 "main.o",
989 ]),
990 );
991 match result {
992 ParsedInvocation::Cacheable(c) => {
993 assert_eq!(c.source_file, NormalizedPath::new("main.cpp"));
994 assert_eq!(c.output_file, NormalizedPath::new("main.o"));
995 }
996 other => panic!("expected Cacheable, got: {other:?}"),
997 }
998 }
999
1000 #[test]
1003 fn sticky_header_mode_cpp_not_spuriously_pch() {
1004 let result = parse_invocation(
1009 "clang++",
1010 &args(&[
1011 "-x",
1012 "c++-header",
1013 "pch.h",
1014 "-o",
1015 "pch.h.pch",
1016 "-x",
1017 "c++",
1018 "-c",
1019 "main.cpp",
1020 "-o",
1021 "main.o",
1022 ]),
1023 );
1024 match &result {
1028 ParsedInvocation::MultiFile { compilations, .. } => {
1029 assert_eq!(compilations.len(), 2);
1030 assert_eq!(compilations[0].source_file, NormalizedPath::new("pch.h"));
1032 assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
1034 }
1035 other => panic!("expected MultiFile, got: {other:?}"),
1036 }
1037 }
1038
1039 #[test]
1040 fn sticky_header_mode_non_source_not_captured_after_reset() {
1041 let result = parse_invocation(
1047 "clang++",
1048 &args(&["-x", "c++-header", "pch.h", "-x", "c++", "-c", "main.cpp"]),
1049 );
1050 match &result {
1051 ParsedInvocation::MultiFile { compilations, .. } => {
1052 assert_eq!(compilations.len(), 2);
1053 assert_eq!(compilations[0].source_file, NormalizedPath::new("pch.h"));
1054 assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
1055 }
1056 other => panic!("expected MultiFile, got: {other:?}"),
1057 }
1058 }
1059
1060 #[test]
1061 fn sticky_header_mode_no_c_flag_after_reset_is_non_cacheable() {
1062 let result = parse_invocation(
1067 "clang++",
1068 &args(&["-x", "c++-header", "-x", "c++", "main.cpp", "-o", "main"]),
1069 );
1070 assert!(
1071 matches!(result, ParsedInvocation::NonCacheable { .. }),
1072 "after -x c++ reset, no -c should be non-cacheable, got: {result:?}"
1073 );
1074 }
1075
1076 #[test]
1077 fn header_unit_c_is_cacheable() {
1078 let result = parse_invocation(
1081 "clang++",
1082 &args(&["-x", "c-header-unit", "foo.h", "-o", "foo.pcm"]),
1083 );
1084 match result {
1085 ParsedInvocation::Cacheable(c) => {
1086 assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
1087 assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
1088 }
1089 other => panic!("expected cacheable, got: {other:?}"),
1090 }
1091 }
1092
1093 #[test]
1094 fn header_unit_cpp_is_cacheable() {
1095 let result = parse_invocation(
1097 "clang++",
1098 &args(&["-x", "c++-header-unit", "foo.h", "-o", "foo.pcm"]),
1099 );
1100 match result {
1101 ParsedInvocation::Cacheable(c) => {
1102 assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
1103 assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
1104 }
1105 other => panic!("expected cacheable, got: {other:?}"),
1106 }
1107 }
1108
1109 #[test]
1110 fn detect_clang_family() {
1111 assert_eq!(detect_family("clang++"), CompilerFamily::Clang);
1112 assert_eq!(detect_family("/usr/bin/clang"), CompilerFamily::Clang);
1113 assert_eq!(detect_family("gcc"), CompilerFamily::Gcc);
1114 assert_eq!(detect_family("g++"), CompilerFamily::Gcc);
1115 }
1116
1117 #[test]
1118 fn detect_emcc_family() {
1119 assert_eq!(detect_family("emcc"), CompilerFamily::Clang);
1120 assert_eq!(detect_family("em++"), CompilerFamily::Clang);
1121 assert_eq!(detect_family("/usr/bin/emcc"), CompilerFamily::Clang);
1122 assert_eq!(detect_family("emcc.exe"), CompilerFamily::Clang);
1123 assert!(CompilerFamily::Clang.supports_depfile());
1125 }
1126
1127 #[test]
1128 fn detect_msvc_family() {
1129 assert_eq!(detect_family("cl"), CompilerFamily::Msvc);
1130 assert_eq!(detect_family("C:\\MSVC\\cl"), CompilerFamily::Msvc);
1131 }
1132
1133 #[test]
1134 fn detect_msvc_case_insensitive() {
1135 assert_eq!(detect_family("CL"), CompilerFamily::Msvc);
1139 assert_eq!(detect_family("CL.EXE"), CompilerFamily::Msvc);
1140 assert_eq!(detect_family("Cl.exe"), CompilerFamily::Msvc);
1141 assert_eq!(detect_family("C:\\MSVC\\CL.EXE"), CompilerFamily::Msvc);
1142 assert_eq!(
1143 detect_family("C:\\Program Files\\MSVC\\cl.EXE"),
1144 CompilerFamily::Msvc
1145 );
1146 }
1147
1148 #[test]
1149 fn gcc_supports_depfile() {
1150 assert!(CompilerFamily::Gcc.supports_depfile());
1151 }
1152
1153 #[test]
1154 fn clang_supports_depfile() {
1155 assert!(CompilerFamily::Clang.supports_depfile());
1156 }
1157
1158 #[test]
1159 fn msvc_no_depfile() {
1160 assert!(!CompilerFamily::Msvc.supports_depfile());
1161 }
1162
1163 #[test]
1166 fn pch_default_output_clang() {
1167 let result = parse_invocation("clang++", &args(&["-x", "c++-header", "src/pch.h"]));
1169 match result {
1170 ParsedInvocation::Cacheable(c) => {
1171 assert_eq!(c.source_file, NormalizedPath::new("src/pch.h"));
1172 assert_eq!(c.output_file, NormalizedPath::new("pch.h.pch"));
1173 }
1174 other => panic!("expected cacheable, got: {other:?}"),
1175 }
1176 }
1177
1178 #[test]
1179 fn pch_default_output_gcc() {
1180 let result = parse_invocation("gcc", &args(&["-x", "c-header", "src/pch.h"]));
1182 match result {
1183 ParsedInvocation::Cacheable(c) => {
1184 assert_eq!(c.source_file, NormalizedPath::new("src/pch.h"));
1185 assert_eq!(c.output_file, NormalizedPath::new("pch.h.gch"));
1186 }
1187 other => panic!("expected cacheable, got: {other:?}"),
1188 }
1189 }
1190
1191 #[test]
1192 fn pch_default_output_strips_directory() {
1193 let result = parse_invocation(
1197 "clang++",
1198 &args(&["-x", "c++-header", "src/fl/audio/fft/fft.h"]),
1199 );
1200 match result {
1201 ParsedInvocation::Cacheable(c) => {
1202 assert_eq!(c.source_file, NormalizedPath::new("src/fl/audio/fft/fft.h"));
1203 assert_eq!(c.output_file, NormalizedPath::new("fft.h.pch"));
1204 }
1205 other => panic!("expected cacheable, got: {other:?}"),
1206 }
1207 }
1208
1209 #[test]
1210 fn pch_default_output_absolute_path_strips_to_filename() {
1211 let result = parse_invocation(
1215 "clang++",
1216 &args(&["-x", "c++-header", "/abs/path/src/pch.h"]),
1217 );
1218 match result {
1219 ParsedInvocation::Cacheable(c) => {
1220 assert_eq!(c.source_file, NormalizedPath::new("/abs/path/src/pch.h"));
1221 assert_eq!(c.output_file, NormalizedPath::new("pch.h.pch"));
1222 }
1223 other => panic!("expected cacheable, got: {other:?}"),
1224 }
1225 }
1226
1227 #[test]
1228 fn pch_default_output_explicit_o_unchanged() {
1229 let result = parse_invocation(
1231 "clang++",
1232 &args(&["-x", "c++-header", "pch.h", "-o", "build/pch.h.pch"]),
1233 );
1234 match result {
1235 ParsedInvocation::Cacheable(c) => {
1236 assert_eq!(c.source_file, NormalizedPath::new("pch.h"));
1237 assert_eq!(c.output_file, NormalizedPath::new("build/pch.h.pch"));
1238 }
1239 other => panic!("expected cacheable, got: {other:?}"),
1240 }
1241 }
1242
1243 #[test]
1244 fn normal_compile_default_output_unchanged() {
1245 let result = parse_invocation("gcc", &args(&["-c", "foo.cpp"]));
1247 match result {
1248 ParsedInvocation::Cacheable(c) => {
1249 assert_eq!(c.output_file, NormalizedPath::new("foo.o"));
1250 }
1251 other => panic!("expected cacheable, got: {other:?}"),
1252 }
1253 }
1254
1255 #[test]
1258 fn concatenated_o_flag_parsed() {
1259 let result = parse_invocation("clang", &args(&["-c", "foo.cpp", "-obuild/foo.o"]));
1261 match result {
1262 ParsedInvocation::Cacheable(c) => {
1263 assert_eq!(c.output_file, NormalizedPath::new("build/foo.o"));
1264 }
1265 other => panic!("expected cacheable, got: {other:?}"),
1266 }
1267 }
1268
1269 #[test]
1270 fn concatenated_o_flag_pch() {
1271 let result = parse_invocation(
1275 "clang++",
1276 &args(&["-x", "c++-header", "pch.h", "-obuild/pch.h.pch"]),
1277 );
1278 match result {
1279 ParsedInvocation::Cacheable(c) => {
1280 assert_eq!(c.output_file, NormalizedPath::new("build/pch.h.pch"));
1281 }
1282 other => panic!("expected cacheable, got: {other:?}"),
1283 }
1284 }
1285
1286 #[test]
1289 fn all_flags_preserved() {
1290 let input = args(&[
1294 "-c",
1295 "foo.cpp",
1296 "-o",
1297 "foo.o",
1298 "-Wall",
1299 "-Wextra",
1300 "-O2",
1301 "-Xclang",
1302 "-fno-spell-checking",
1303 "-std=c++17",
1304 "-DFOO=bar",
1305 "-I/usr/include",
1306 "-isystem",
1307 "/usr/local/include",
1308 "-unknown-future-flag",
1309 ]);
1310 let result = parse_invocation("clang++", &input);
1311 match result {
1312 ParsedInvocation::Cacheable(c) => {
1313 assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
1314 assert_eq!(c.output_file, NormalizedPath::new("foo.o"));
1315 assert!(c.unknown_flags.contains(&"-Wall".to_string()));
1317 assert!(c.unknown_flags.contains(&"-Wextra".to_string()));
1318 assert!(c.unknown_flags.contains(&"-O2".to_string()));
1319 assert!(c
1320 .unknown_flags
1321 .contains(&"-unknown-future-flag".to_string()));
1322 assert!(c.unknown_flags.contains(&"-std=c++17".to_string()));
1326 assert!(c.unknown_flags.contains(&"-DFOO=bar".to_string()));
1327 assert!(c.unknown_flags.contains(&"-I/usr/include".to_string()));
1328 }
1329 other => panic!("expected cacheable, got: {other:?}"),
1330 }
1331 }
1332
1333 #[test]
1334 fn xclang_value_not_misidentified_as_source() {
1335 let result = parse_invocation(
1339 "clang++",
1340 &args(&[
1341 "-c",
1342 "foo.cpp",
1343 "-Xclang",
1344 "-fno-spell-checking",
1345 "-o",
1346 "foo.o",
1347 ]),
1348 );
1349 match result {
1350 ParsedInvocation::Cacheable(c) => {
1351 assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
1352 }
1354 other => panic!("expected cacheable, got: {other:?}"),
1355 }
1356 }
1357
1358 #[test]
1359 fn mllvm_value_not_misidentified_as_source() {
1360 let result = parse_invocation(
1361 "clang++",
1362 &args(&["-c", "foo.cpp", "-mllvm", "-some-llvm-opt", "-o", "foo.o"]),
1363 );
1364 match result {
1365 ParsedInvocation::Cacheable(c) => {
1366 assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
1367 }
1368 other => panic!("expected cacheable, got: {other:?}"),
1369 }
1370 }
1371
1372 #[test]
1375 fn pch_output_path_mismatch_repro() {
1376 let result = parse_invocation(
1382 "clang++",
1383 &args(&["-x", "c++-header", "src/fl/fx/2d/flowfield_q31.h"]),
1384 );
1385 match result {
1386 ParsedInvocation::Cacheable(c) => {
1387 assert_eq!(c.output_file, NormalizedPath::new("flowfield_q31.h.pch"));
1388 assert_eq!(
1389 c.source_file,
1390 NormalizedPath::new("src/fl/fx/2d/flowfield_q31.h")
1391 );
1392 }
1393 other => panic!("expected cacheable, got: {other:?}"),
1394 }
1395 }
1396
1397 #[test]
1400 fn detect_rustc_family() {
1401 assert_eq!(detect_family("rustc"), CompilerFamily::Rustc);
1402 assert_eq!(detect_family("/usr/bin/rustc"), CompilerFamily::Rustc);
1403 assert_eq!(detect_family("rustc.exe"), CompilerFamily::Rustc);
1404 assert_eq!(
1405 detect_family("C:\\rustup\\rustc.exe"),
1406 CompilerFamily::Rustc
1407 );
1408 }
1409
1410 #[test]
1411 fn rustc_no_depfile_support() {
1412 assert!(!CompilerFamily::Rustc.supports_depfile());
1414 }
1415
1416 #[test]
1417 fn rustc_no_pch_extension() {
1418 assert_eq!(CompilerFamily::Rustc.pch_extension(), None);
1419 }
1420
1421 #[test]
1424 fn rustc_lib_crate_is_cacheable() {
1425 let result = parse_invocation(
1426 "rustc",
1427 &args(&[
1428 "--edition",
1429 "2021",
1430 "--crate-type",
1431 "lib",
1432 "--emit=dep-info,metadata,link",
1433 "-C",
1434 "opt-level=2",
1435 "src/lib.rs",
1436 ]),
1437 );
1438 match result {
1439 ParsedInvocation::Cacheable(c) => {
1440 assert_eq!(c.family, CompilerFamily::Rustc);
1441 assert_eq!(c.source_file, NormalizedPath::new("src/lib.rs"));
1442 }
1443 other => panic!("expected cacheable, got: {other:?}"),
1444 }
1445 }
1446
1447 #[test]
1448 fn rustc_rlib_crate_is_cacheable() {
1449 let result = parse_invocation("rustc", &args(&["--crate-type", "rlib", "src/lib.rs"]));
1450 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1451 }
1452
1453 #[test]
1454 fn rustc_staticlib_crate_is_cacheable() {
1455 let result = parse_invocation("rustc", &args(&["--crate-type", "staticlib", "src/lib.rs"]));
1456 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1457 }
1458
1459 #[test]
1460 fn rustc_bin_crate_is_non_cacheable() {
1461 let result = parse_invocation("rustc", &args(&["--crate-type", "bin", "src/main.rs"]));
1462 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1463 }
1464
1465 #[test]
1466 fn rustc_dylib_is_non_cacheable() {
1467 let result = parse_invocation("rustc", &args(&["--crate-type", "dylib", "src/lib.rs"]));
1468 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1469 }
1470
1471 #[test]
1472 fn rustc_proc_macro_is_non_cacheable() {
1473 let result = parse_invocation(
1474 "rustc",
1475 &args(&["--crate-type", "proc-macro", "src/lib.rs"]),
1476 );
1477 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1478 }
1479
1480 #[test]
1481 fn rustc_cdylib_is_non_cacheable() {
1482 let result = parse_invocation("rustc", &args(&["--crate-type", "cdylib", "src/lib.rs"]));
1483 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1484 }
1485
1486 #[test]
1487 fn rustc_no_crate_type_defaults_to_bin_non_cacheable() {
1488 let result = parse_invocation("rustc", &args(&["src/main.rs"]));
1490 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1491 }
1492
1493 #[test]
1494 fn rustc_incremental_is_cacheable() {
1495 let result = parse_invocation(
1497 "rustc",
1498 &args(&[
1499 "--crate-type",
1500 "lib",
1501 "-C",
1502 "incremental=/tmp/incr",
1503 "src/lib.rs",
1504 ]),
1505 );
1506 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1507 }
1508
1509 #[test]
1510 fn rustc_no_source_is_non_cacheable() {
1511 let result = parse_invocation("rustc", &args(&["--version"]));
1512 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1513 }
1514
1515 #[test]
1516 fn rustc_emit_metadata_is_cacheable() {
1517 let result = parse_invocation(
1519 "rustc",
1520 &args(&["--crate-type", "lib", "--emit=metadata", "src/lib.rs"]),
1521 );
1522 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1523 }
1524
1525 #[test]
1526 fn rustc_output_with_explicit_o() {
1527 let result = parse_invocation(
1528 "rustc",
1529 &args(&["--crate-type", "lib", "src/lib.rs", "-o", "libfoo.rlib"]),
1530 );
1531 match result {
1532 ParsedInvocation::Cacheable(c) => {
1533 assert_eq!(c.output_file, NormalizedPath::new("libfoo.rlib"));
1534 }
1535 other => panic!("expected cacheable, got: {other:?}"),
1536 }
1537 }
1538
1539 #[test]
1540 fn rustc_metadata_only_output_is_rmeta() {
1541 let result = parse_invocation(
1543 "rustc",
1544 &args(&[
1545 "--crate-type",
1546 "lib",
1547 "--crate-name",
1548 "mylib",
1549 "--emit=dep-info,metadata",
1550 "--out-dir",
1551 "/target/debug/deps",
1552 "-C",
1553 "extra-filename=-abc123",
1554 "src/lib.rs",
1555 ]),
1556 );
1557 match result {
1558 ParsedInvocation::Cacheable(c) => {
1559 assert_eq!(
1560 c.output_file,
1561 NormalizedPath::new("/target/debug/deps/libmylib-abc123.rmeta")
1562 );
1563 }
1564 other => panic!("expected cacheable, got: {other:?}"),
1565 }
1566 }
1567
1568 #[test]
1569 fn rustc_output_from_out_dir() {
1570 let result = parse_invocation(
1571 "rustc",
1572 &args(&[
1573 "--crate-type",
1574 "lib",
1575 "--crate-name",
1576 "mylib",
1577 "--out-dir",
1578 "/target/debug/deps",
1579 "-C",
1580 "extra-filename=-abc123",
1581 "src/lib.rs",
1582 ]),
1583 );
1584 match result {
1585 ParsedInvocation::Cacheable(c) => {
1586 assert_eq!(
1587 c.output_file,
1588 NormalizedPath::new("/target/debug/deps/libmylib-abc123.rlib")
1589 );
1590 }
1591 other => panic!("expected cacheable, got: {other:?}"),
1592 }
1593 }
1594
1595 #[test]
1596 fn rustc_full_cargo_invocation_cacheable() {
1597 let result = parse_invocation(
1599 "rustc",
1600 &args(&[
1601 "--edition",
1602 "2021",
1603 "--crate-type",
1604 "lib",
1605 "--crate-name",
1606 "serde",
1607 "--emit=dep-info,metadata,link",
1608 "-C",
1609 "opt-level=2",
1610 "-C",
1611 "metadata=abc123def",
1612 "-C",
1613 "extra-filename=-abc123def",
1614 "--out-dir",
1615 "/target/release/deps",
1616 "-L",
1617 "dependency=/target/release/deps",
1618 "--extern",
1619 "serde_derive=/target/release/deps/libserde_derive-xyz.so",
1620 "--cap-lints",
1621 "allow",
1622 "--cfg",
1623 "feature=\"derive\"",
1624 "--cfg",
1625 "feature=\"std\"",
1626 "src/lib.rs",
1627 ]),
1628 );
1629 match result {
1630 ParsedInvocation::Cacheable(c) => {
1631 assert_eq!(c.family, CompilerFamily::Rustc);
1632 assert_eq!(c.source_file, NormalizedPath::new("src/lib.rs"));
1633 assert_eq!(
1634 c.output_file,
1635 NormalizedPath::new("/target/release/deps/libserde-abc123def.rlib")
1636 );
1637 }
1638 other => panic!("expected cacheable, got: {other:?}"),
1639 }
1640 }
1641
1642 #[test]
1643 fn rustc_original_args_preserved() {
1644 let input = args(&["--edition", "2021", "--crate-type", "lib", "src/lib.rs"]);
1645 let result = parse_invocation("rustc", &input);
1646 match result {
1647 ParsedInvocation::Cacheable(c) => {
1648 assert_eq!(*c.original_args, *input);
1649 }
1650 other => panic!("expected cacheable, got: {other:?}"),
1651 }
1652 }
1653
1654 #[test]
1655 fn rustc_equal_form_crate_type() {
1656 let result = parse_invocation("rustc", &args(&["--crate-type=lib", "src/lib.rs"]));
1657 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1658 }
1659
1660 #[test]
1661 fn rustc_concatenated_c_incremental_is_cacheable() {
1662 let result = parse_invocation(
1664 "rustc",
1665 &args(&["--crate-type", "lib", "-Cincremental=/tmp", "src/lib.rs"]),
1666 );
1667 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1668 }
1669
1670 #[test]
1671 fn rustc_comma_separated_crate_type_all_cacheable() {
1672 let result = parse_invocation("rustc", &args(&["--crate-type", "lib,rlib", "src/lib.rs"]));
1673 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1674 }
1675
1676 #[test]
1677 fn rustc_comma_separated_crate_type_mixed_non_cacheable() {
1678 let result = parse_invocation("rustc", &args(&["--crate-type", "lib,dylib", "src/lib.rs"]));
1680 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1681 }
1682
1683 #[test]
1684 fn rustc_comma_separated_crate_type_equals_form() {
1685 let result = parse_invocation(
1686 "rustc",
1687 &args(&["--crate-type=lib,staticlib", "src/lib.rs"]),
1688 );
1689 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1690 }
1691
1692 #[test]
1693 fn rustc_test_flag_makes_non_cacheable() {
1694 let result = parse_invocation(
1696 "rustc",
1697 &args(&["--crate-type", "lib", "--test", "src/lib.rs"]),
1698 );
1699 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1704 }
1705
1706 #[test]
1709 fn detect_clippy_driver_family() {
1710 assert_eq!(detect_family("clippy-driver"), CompilerFamily::Rustc);
1711 assert_eq!(
1712 detect_family(
1713 "/home/user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/clippy-driver"
1714 ),
1715 CompilerFamily::Rustc
1716 );
1717 assert_eq!(
1718 detect_family("C:\\Users\\user\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\bin\\clippy-driver.exe"),
1719 CompilerFamily::Rustc
1720 );
1721 }
1722
1723 #[test]
1724 fn detect_clippy_driver_versioned() {
1725 assert_eq!(detect_family("clippy-driver-1.78"), CompilerFamily::Rustc);
1727 }
1728
1729 #[test]
1730 fn clippy_driver_cacheable_lib() {
1731 let result = parse_invocation(
1733 "clippy-driver",
1734 &args(&[
1735 "--crate-name",
1736 "mycrate",
1737 "--crate-type",
1738 "lib",
1739 "--emit=metadata,dep-info",
1740 "--out-dir",
1741 "target/debug/deps",
1742 "-C",
1743 "extra-filename=-abc123",
1744 "src/lib.rs",
1745 ]),
1746 );
1747 match result {
1748 ParsedInvocation::Cacheable(c) => {
1749 assert_eq!(c.family, CompilerFamily::Rustc);
1750 assert_eq!(c.source_file, NormalizedPath::new("src/lib.rs"));
1751 assert!(c.output_file.to_str().unwrap().ends_with(".rmeta"));
1753 }
1754 other => panic!("expected cacheable, got: {other:?}"),
1755 }
1756 }
1757
1758 #[test]
1759 fn clippy_driver_non_cacheable_bin() {
1760 let result = parse_invocation(
1762 "clippy-driver",
1763 &args(&[
1764 "--crate-name",
1765 "mybin",
1766 "--crate-type",
1767 "bin",
1768 "src/main.rs",
1769 ]),
1770 );
1771 assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
1772 }
1773
1774 #[test]
1775 fn clippy_driver_with_lint_flags() {
1776 let result = parse_invocation(
1778 "clippy-driver",
1779 &args(&[
1780 "--crate-name",
1781 "mycrate",
1782 "--crate-type",
1783 "lib",
1784 "-W",
1785 "clippy::all",
1786 "-D",
1787 "clippy::unwrap_used",
1788 "-A",
1789 "clippy::too_many_arguments",
1790 "src/lib.rs",
1791 ]),
1792 );
1793 assert!(matches!(result, ParsedInvocation::Cacheable(_)));
1794 }
1795
1796 #[test]
1801 fn cppm_extension_is_cacheable() {
1802 let result = parse_invocation("clang++", &args(&["-c", "module.cppm", "-o", "module.pcm"]));
1803 match result {
1804 ParsedInvocation::Cacheable(c) => {
1805 assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
1806 assert_eq!(c.output_file, NormalizedPath::new("module.pcm"));
1807 }
1808 other => panic!("expected cacheable, got: {other:?}"),
1809 }
1810 }
1811
1812 #[test]
1813 fn ixx_extension_is_cacheable() {
1814 let result = parse_invocation("g++", &args(&["-c", "module.ixx", "-o", "module.o"]));
1815 match result {
1816 ParsedInvocation::Cacheable(c) => {
1817 assert_eq!(c.source_file, NormalizedPath::new("module.ixx"));
1818 assert_eq!(c.output_file, NormalizedPath::new("module.o"));
1819 }
1820 other => panic!("expected cacheable, got: {other:?}"),
1821 }
1822 }
1823
1824 #[test]
1825 fn cppm_default_output_with_precompile_is_pcm() {
1826 let result = parse_invocation("clang++", &args(&["--precompile", "module.cppm"]));
1828 match result {
1829 ParsedInvocation::Cacheable(c) => {
1830 assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
1831 assert_eq!(c.output_file, NormalizedPath::new("module.pcm"));
1832 }
1833 other => panic!("expected cacheable, got: {other:?}"),
1834 }
1835 }
1836
1837 #[test]
1838 fn cppm_default_output_with_c_flag_is_object() {
1839 let result = parse_invocation("clang++", &args(&["-c", "module.cppm"]));
1841 match result {
1842 ParsedInvocation::Cacheable(c) => {
1843 assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
1844 assert_eq!(c.output_file, NormalizedPath::new("module.o"));
1845 }
1846 other => panic!("expected cacheable, got: {other:?}"),
1847 }
1848 }
1849
1850 #[test]
1851 fn cppm_multi_file() {
1852 let result = parse_invocation("clang++", &args(&["-c", "a.cppm", "b.cppm"]));
1853 match result {
1854 ParsedInvocation::MultiFile { compilations, .. } => {
1855 assert_eq!(compilations.len(), 2);
1856 assert_eq!(compilations[0].source_file, NormalizedPath::new("a.cppm"));
1857 assert_eq!(compilations[1].source_file, NormalizedPath::new("b.cppm"));
1858 }
1859 other => panic!("expected MultiFile, got: {other:?}"),
1860 }
1861 }
1862
1863 #[test]
1866 fn x_cpp_module_with_precompile_is_cacheable() {
1867 let result = parse_invocation(
1868 "clang++",
1869 &args(&[
1870 "-x",
1871 "c++-module",
1872 "--precompile",
1873 "interface.cpp",
1874 "-o",
1875 "interface.pcm",
1876 ]),
1877 );
1878 match result {
1879 ParsedInvocation::Cacheable(c) => {
1880 assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
1881 assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
1882 }
1883 other => panic!("expected cacheable, got: {other:?}"),
1884 }
1885 }
1886
1887 #[test]
1888 fn x_cpp_module_with_c_flag_is_cacheable() {
1889 let result = parse_invocation(
1890 "clang++",
1891 &args(&[
1892 "-x",
1893 "c++-module",
1894 "-c",
1895 "interface.cpp",
1896 "-o",
1897 "interface.o",
1898 ]),
1899 );
1900 match result {
1901 ParsedInvocation::Cacheable(c) => {
1902 assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
1903 assert_eq!(c.output_file, NormalizedPath::new("interface.o"));
1904 }
1905 other => panic!("expected cacheable, got: {other:?}"),
1906 }
1907 }
1908
1909 #[test]
1910 fn x_cpp_module_without_c_or_precompile_is_non_cacheable() {
1911 let result = parse_invocation(
1914 "clang++",
1915 &args(&["-x", "c++-module", "interface.cpp", "-o", "interface"]),
1916 );
1917 assert!(
1918 matches!(result, ParsedInvocation::NonCacheable { .. }),
1919 "-x c++-module without -c or --precompile should be non-cacheable, got: {result:?}"
1920 );
1921 }
1922
1923 #[test]
1924 fn x_cpp_module_accepts_non_source_extension() {
1925 let result = parse_invocation(
1928 "clang++",
1929 &args(&["-x", "c++-module", "--precompile", "interface.mpp"]),
1930 );
1931 match result {
1932 ParsedInvocation::Cacheable(c) => {
1933 assert_eq!(c.source_file, NormalizedPath::new("interface.mpp"));
1934 }
1935 other => panic!("expected cacheable, got: {other:?}"),
1936 }
1937 }
1938
1939 #[test]
1940 fn x_cpp_module_default_output_precompile() {
1941 let result = parse_invocation(
1943 "clang++",
1944 &args(&["-x", "c++-module", "--precompile", "interface.cpp"]),
1945 );
1946 match result {
1947 ParsedInvocation::Cacheable(c) => {
1948 assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
1949 }
1950 other => panic!("expected cacheable, got: {other:?}"),
1951 }
1952 }
1953
1954 #[test]
1955 fn x_cpp_module_default_output_c_flag() {
1956 let result = parse_invocation(
1958 "clang++",
1959 &args(&["-x", "c++-module", "-c", "interface.cpp"]),
1960 );
1961 match result {
1962 ParsedInvocation::Cacheable(c) => {
1963 assert_eq!(c.output_file, NormalizedPath::new("interface.o"));
1964 }
1965 other => panic!("expected cacheable, got: {other:?}"),
1966 }
1967 }
1968
1969 #[test]
1970 fn x_cpp_module_reset_by_x_cpp() {
1971 let result = parse_invocation(
1973 "clang++",
1974 &args(&[
1975 "-x",
1976 "c++-module",
1977 "--precompile",
1978 "interface.mpp",
1979 "-x",
1980 "c++",
1981 "-c",
1982 "main.cpp",
1983 ]),
1984 );
1985 match result {
1986 ParsedInvocation::MultiFile { compilations, .. } => {
1987 assert_eq!(compilations.len(), 2);
1988 assert_eq!(
1989 compilations[0].source_file,
1990 NormalizedPath::new("interface.mpp")
1991 );
1992 assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
1993 }
1994 other => panic!("expected MultiFile, got: {other:?}"),
1995 }
1996 }
1997
1998 #[test]
1999 fn x_cpp_module_implies_compilation_with_precompile() {
2000 let result = parse_invocation(
2002 "clang++",
2003 &args(&[
2004 "-x",
2005 "c++-module",
2006 "--precompile",
2007 "interface.cpp",
2008 "-o",
2009 "interface.pcm",
2010 ]),
2011 );
2012 match result {
2013 ParsedInvocation::Cacheable(c) => {
2014 assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
2015 assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
2016 }
2017 other => panic!("expected cacheable, got: {other:?}"),
2018 }
2019 }
2020
2021 #[test]
2024 fn x_cpp_header_unit_with_precompile_is_cacheable() {
2025 let result = parse_invocation(
2026 "clang++",
2027 &args(&[
2028 "-x",
2029 "c++-header-unit",
2030 "--precompile",
2031 "foo.h",
2032 "-o",
2033 "foo.pcm",
2034 ]),
2035 );
2036 match result {
2037 ParsedInvocation::Cacheable(c) => {
2038 assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
2039 assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
2040 }
2041 other => panic!("expected cacheable, got: {other:?}"),
2042 }
2043 }
2044
2045 #[test]
2046 fn x_c_header_unit_with_c_flag_is_cacheable() {
2047 let result = parse_invocation(
2048 "gcc",
2049 &args(&["-x", "c-header-unit", "-c", "foo.h", "-o", "foo.pcm"]),
2050 );
2051 match result {
2052 ParsedInvocation::Cacheable(c) => {
2053 assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
2054 assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
2055 }
2056 other => panic!("expected cacheable, got: {other:?}"),
2057 }
2058 }
2059
2060 #[test]
2061 fn x_cpp_header_unit_default_output_is_pcm() {
2062 let result = parse_invocation(
2064 "clang++",
2065 &args(&["-x", "c++-header-unit", "--precompile", "foo.h"]),
2066 );
2067 match result {
2068 ParsedInvocation::Cacheable(c) => {
2069 assert_eq!(c.output_file, NormalizedPath::new("foo.h.pcm"));
2070 }
2071 other => panic!("expected cacheable, got: {other:?}"),
2072 }
2073 }
2074
2075 #[test]
2076 fn x_cpp_header_unit_implies_compilation() {
2077 let result = parse_invocation(
2079 "clang++",
2080 &args(&["-x", "c++-header-unit", "foo.h", "-o", "foo.pcm"]),
2081 );
2082 match result {
2083 ParsedInvocation::Cacheable(c) => {
2084 assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
2085 assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
2086 }
2087 other => panic!("expected cacheable, got: {other:?}"),
2088 }
2089 }
2090
2091 #[test]
2094 fn precompile_on_normal_cpp_is_cacheable() {
2095 let result = parse_invocation("clang++", &args(&["--precompile", "foo.cpp"]));
2097 match result {
2098 ParsedInvocation::Cacheable(c) => {
2099 assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
2100 assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
2101 }
2102 other => panic!("expected cacheable, got: {other:?}"),
2103 }
2104 }
2105
2106 #[test]
2107 fn precompile_without_source_is_non_cacheable() {
2108 let result = parse_invocation("clang++", &args(&["--precompile", "-O2"]));
2109 assert!(
2110 matches!(result, ParsedInvocation::NonCacheable { .. }),
2111 "--precompile without source should be non-cacheable, got: {result:?}"
2112 );
2113 }
2114
2115 #[test]
2116 fn precompile_and_c_flag_together() {
2117 let result = parse_invocation(
2120 "clang++",
2121 &args(&["--precompile", "-c", "module.cppm", "-o", "module.pcm"]),
2122 );
2123 match result {
2124 ParsedInvocation::Cacheable(c) => {
2125 assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
2126 assert_eq!(c.output_file, NormalizedPath::new("module.pcm"));
2127 }
2128 other => panic!("expected cacheable, got: {other:?}"),
2129 }
2130 }
2131
2132 #[test]
2135 fn gcc_fmodules_ts_with_cppm_is_cacheable() {
2136 let result = parse_invocation(
2138 "g++",
2139 &args(&["-fmodules-ts", "-c", "module.cppm", "-o", "module.o"]),
2140 );
2141 match result {
2142 ParsedInvocation::Cacheable(c) => {
2143 assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
2144 assert_eq!(c.output_file, NormalizedPath::new("module.o"));
2145 assert!(c.unknown_flags.contains(&"-fmodules-ts".to_string()));
2146 }
2147 other => panic!("expected cacheable, got: {other:?}"),
2148 }
2149 }
2150
2151 #[test]
2152 fn gcc_fmodules_ts_with_x_module_precompile() {
2153 let result = parse_invocation(
2154 "g++",
2155 &args(&[
2156 "-fmodules-ts",
2157 "-x",
2158 "c++-module",
2159 "--precompile",
2160 "interface.cpp",
2161 ]),
2162 );
2163 match result {
2164 ParsedInvocation::Cacheable(c) => {
2165 assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
2166 assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
2167 }
2168 other => panic!("expected cacheable, got: {other:?}"),
2169 }
2170 }
2171}