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