#![allow(clippy::missing_errors_doc)]
pub mod arduino;
pub mod parse_archiver;
pub mod parse_linker;
pub mod parse_rustfmt;
pub mod response_file;
pub mod strict_paths;
use std::sync::Arc;
use zccache_core::NormalizedPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompilerFamily {
Gcc,
Clang,
Msvc,
Rustc,
Rustfmt,
}
impl CompilerFamily {
#[must_use]
pub fn supports_depfile(&self) -> bool {
matches!(self, CompilerFamily::Gcc | CompilerFamily::Clang)
}
#[must_use]
pub fn pch_extension(&self) -> Option<&'static str> {
match self {
CompilerFamily::Gcc => Some("gch"),
CompilerFamily::Clang => Some("pch"),
CompilerFamily::Msvc | CompilerFamily::Rustc | CompilerFamily::Rustfmt => None,
}
}
#[must_use]
pub fn is_formatter(&self) -> bool {
matches!(self, CompilerFamily::Rustfmt)
}
}
#[derive(Debug, Clone)]
pub enum ParsedInvocation {
Cacheable(CacheableCompilation),
MultiFile {
compilations: Vec<CacheableCompilation>,
original_args: Arc<[String]>,
source_indices: Vec<usize>,
},
NonCacheable {
reason: String,
},
}
#[derive(Debug, Clone)]
pub struct CacheableCompilation {
pub compiler: NormalizedPath,
pub family: CompilerFamily,
pub source_file: NormalizedPath,
pub output_file: NormalizedPath,
pub original_args: Arc<[String]>,
pub unknown_flags: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SourceMode {
Normal,
Header,
HeaderUnit,
Module,
}
impl SourceMode {
pub(crate) fn implies_compilation(self) -> bool {
matches!(self, SourceMode::Header | SourceMode::HeaderUnit)
}
}
fn source_mode_from_language(lang: &str) -> Option<SourceMode> {
match lang {
"c-header" | "c++-header" => Some(SourceMode::Header),
"c-header-unit" | "c++-header-unit" => Some(SourceMode::HeaderUnit),
"c++-module" => Some(SourceMode::Module),
_ => None,
}
}
const SOURCE_EXTENSIONS: &[&str] = &[
"c", "cc", "cpp", "cxx", "c++", "C", "m", "mm", "i", "ii", "cppm", "ixx",
];
const MODULE_EXTENSIONS: &[&str] = &["cppm", "ixx"];
fn source_mode_from_extension(path: &str) -> SourceMode {
if let Some(ext) = std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
{
if MODULE_EXTENSIONS.contains(&ext) {
return SourceMode::Module;
}
}
SourceMode::Normal
}
#[must_use]
pub fn detect_family(compiler: &str) -> CompilerFamily {
let basename = compiler.rsplit(['/', '\\']).next().unwrap_or(compiler);
let name = match basename.rsplit_once('.') {
Some((stem, _)) => stem,
None => basename,
};
if name == "rustfmt" || name.starts_with("rustfmt-") {
CompilerFamily::Rustfmt
} else if name == "rustc"
|| name.starts_with("rustc-")
|| name == "clippy-driver"
|| name.starts_with("clippy-driver-")
{
CompilerFamily::Rustc
} else if name.contains("clang") || name == "emcc" || name == "em++" {
CompilerFamily::Clang
} else if name.eq_ignore_ascii_case("cl") {
CompilerFamily::Msvc
} else {
CompilerFamily::Gcc
}
}
fn is_source_file(path: &str) -> bool {
if let Some(ext) = std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
{
SOURCE_EXTENSIONS.contains(&ext)
} else {
false
}
}
fn default_output(
source: &str,
family: CompilerFamily,
mode: SourceMode,
has_precompile: bool,
) -> String {
match mode {
SourceMode::Header => {
if let Some(ext) = family.pch_extension() {
let filename = std::path::Path::new(source)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(source);
return format!("{filename}.{ext}");
}
}
SourceMode::HeaderUnit => {
let filename = std::path::Path::new(source)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(source);
return format!("{filename}.pcm");
}
SourceMode::Module | SourceMode::Normal => {
if has_precompile {
let stem = std::path::Path::new(source)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("a");
return format!("{stem}.pcm");
}
}
}
let stem = std::path::Path::new(source)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("a");
format!("{stem}.o")
}
const FLAGS_WITH_VALUE: &[&str] = &[
"-o",
"-D",
"-U",
"-I",
"-isystem",
"-iquote",
"-idirafter",
"-include",
"-include-pch",
"-isysroot",
"-target",
"--target",
"-MF",
"-MQ",
"-MT",
"-std",
"-x",
"-arch",
"-Xclang",
"-mllvm",
"--serialize-diagnostics",
];
#[must_use]
pub fn parse_invocation(compiler: &str, args: &[String]) -> ParsedInvocation {
let family = detect_family(compiler);
if family == CompilerFamily::Rustfmt {
return ParsedInvocation::NonCacheable {
reason: "rustfmt is handled via the format cache path, not compile cache".to_string(),
};
}
if family == CompilerFamily::Rustc {
return parse_rustc_invocation(compiler, args);
}
let mut has_c_flag = false;
let mut has_precompile_flag = false;
let mut source_files: Vec<(String, usize, SourceMode)> = Vec::new();
let mut output_file: Option<String> = None;
let mut current_mode = SourceMode::Normal;
let mut unknown_flags: Vec<String> = Vec::new();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "-E" || arg == "-M" || arg == "-MM" {
return ParsedInvocation::NonCacheable {
reason: format!("preprocessing-only flag: {arg}"),
};
}
if arg == "-" {
return ParsedInvocation::NonCacheable {
reason: "stdin source not cacheable".to_string(),
};
}
if arg == "-c" {
has_c_flag = true;
i += 1;
continue;
}
if arg == "--precompile" {
has_precompile_flag = true;
i += 1;
continue;
}
if arg == "-o" {
if let Some(next) = args.get(i + 1) {
output_file = Some(next.clone());
i += 2;
} else {
i += 1;
}
continue;
} else if let Some(path) = arg.strip_prefix("-o") {
output_file = Some(path.to_string());
i += 1;
continue;
}
if let Some(&flag) = FLAGS_WITH_VALUE.iter().find(|&&f| f == arg.as_str()) {
if flag == "-x" && i + 1 < args.len() {
current_mode =
source_mode_from_language(&args[i + 1]).unwrap_or(SourceMode::Normal);
}
i += 2;
continue;
}
if arg.starts_with('-') {
unknown_flags.push(arg.clone());
i += 1;
continue;
}
let effective_mode = if current_mode != SourceMode::Normal {
current_mode
} else {
source_mode_from_extension(arg)
};
if is_source_file(arg) || current_mode != SourceMode::Normal {
source_files.push((arg.clone(), i, effective_mode));
}
i += 1;
}
if !has_c_flag && !has_precompile_flag && !current_mode.implies_compilation() {
return ParsedInvocation::NonCacheable {
reason: "no -c flag (likely a link invocation)".to_string(),
};
}
if source_files.is_empty() {
return ParsedInvocation::NonCacheable {
reason: "no source file found".to_string(),
};
}
let family = detect_family(compiler);
if source_files.len() > 1 {
let source_indices: Vec<usize> = source_files.iter().map(|(_, idx, _)| *idx).collect();
let shared_args: Arc<[String]> = Arc::from(args.to_vec());
let compilations = source_files
.iter()
.map(|(src, _, mode)| CacheableCompilation {
compiler: NormalizedPath::new(compiler),
family,
source_file: NormalizedPath::new(src),
output_file: NormalizedPath::new(default_output(
src,
family,
*mode,
has_precompile_flag,
)),
original_args: Arc::clone(&shared_args),
unknown_flags: unknown_flags.clone(),
})
.collect();
return ParsedInvocation::MultiFile {
compilations,
original_args: shared_args,
source_indices,
};
}
let (source, _, mode) = source_files.into_iter().next().unwrap();
let output =
output_file.unwrap_or_else(|| default_output(&source, family, mode, has_precompile_flag));
ParsedInvocation::Cacheable(CacheableCompilation {
compiler: NormalizedPath::new(compiler),
family,
source_file: NormalizedPath::new(source),
output_file: NormalizedPath::new(output),
original_args: Arc::from(args.to_vec()),
unknown_flags,
})
}
const RUSTC_CACHEABLE_CRATE_TYPES: &[&str] = &["lib", "rlib", "staticlib"];
const RUSTC_FLAGS_WITH_VALUE: &[&str] = &[
"--edition",
"--crate-type",
"--crate-name",
"--emit",
"--out-dir",
"--target",
"--cap-lints",
"--extern",
"--error-format",
"--json",
"--color",
"--diagnostic-width",
"--sysroot",
"--cfg",
"--check-cfg",
"-o",
"-L",
"-C",
"-A",
"-W",
"-D",
"-F",
"--codegen",
"--remap-path-prefix",
"--env-set",
];
fn parse_rustc_invocation(compiler: &str, args: &[String]) -> ParsedInvocation {
let mut crate_types: Vec<String> = Vec::new();
let mut source_file: Option<String> = None;
let mut output_file: Option<String> = None;
let mut out_dir: Option<String> = None;
let mut crate_name: Option<String> = None;
let mut extra_filename: Option<String> = None;
let mut emit_types: Vec<String> = Vec::new();
let mut unknown_flags: Vec<String> = Vec::new();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "--crate-type" {
if let Some(next) = args.get(i + 1) {
crate_types.extend(next.split(',').map(|s| s.to_string()));
i += 2;
continue;
}
} else if let Some(val) = arg.strip_prefix("--crate-type=") {
crate_types.extend(val.split(',').map(|s| s.to_string()));
i += 1;
continue;
}
if arg == "--crate-name" {
if let Some(next) = args.get(i + 1) {
crate_name = Some(next.clone());
i += 2;
continue;
}
} else if let Some(val) = arg.strip_prefix("--crate-name=") {
crate_name = Some(val.to_string());
i += 1;
continue;
}
if arg == "--emit" {
if let Some(next) = args.get(i + 1) {
emit_types.extend(next.split(',').map(|s| {
s.split('=').next().unwrap_or(s).to_string()
}));
i += 2;
continue;
}
} else if let Some(val) = arg.strip_prefix("--emit=") {
emit_types.extend(
val.split(',')
.map(|s| s.split('=').next().unwrap_or(s).to_string()),
);
i += 1;
continue;
}
if arg == "--out-dir" {
if let Some(next) = args.get(i + 1) {
out_dir = Some(next.clone());
i += 2;
continue;
}
} else if let Some(val) = arg.strip_prefix("--out-dir=") {
out_dir = Some(val.to_string());
i += 1;
continue;
}
if arg == "-o" {
if let Some(next) = args.get(i + 1) {
output_file = Some(next.clone());
i += 2;
continue;
}
}
if arg == "-C" || arg == "--codegen" {
if let Some(next) = args.get(i + 1) {
if let Some(val) = next.strip_prefix("extra-filename=") {
extra_filename = Some(val.to_string());
}
i += 2;
continue;
}
} else if let Some(rest) = arg.strip_prefix("-C") {
if !rest.is_empty() {
if let Some(val) = rest.strip_prefix("extra-filename=") {
extra_filename = Some(val.to_string());
}
i += 1;
continue;
}
}
if let Some(&_flag) = RUSTC_FLAGS_WITH_VALUE.iter().find(|&&f| f == arg.as_str()) {
i += 2;
continue;
}
if arg.starts_with("--") && arg.contains('=') {
i += 1;
continue;
}
if arg.starts_with('-') {
unknown_flags.push(arg.clone());
i += 1;
continue;
}
if arg.ends_with(".rs") {
source_file = Some(arg.clone());
}
i += 1;
}
let source = match source_file {
Some(s) => s,
None => {
return ParsedInvocation::NonCacheable {
reason: "no .rs source file found".to_string(),
};
}
};
if crate_types.is_empty() {
crate_types.push("bin".to_string());
}
for ct in &crate_types {
if !RUSTC_CACHEABLE_CRATE_TYPES.contains(&ct.as_str()) {
return ParsedInvocation::NonCacheable {
reason: format!("non-cacheable crate type: {ct}"),
};
}
}
let has_link_emit = emit_types.iter().any(|t| t == "link");
let primary_ext = if !has_link_emit && emit_types.iter().any(|t| t == "metadata") {
"rmeta"
} else {
match crate_types.first().map(|s| s.as_str()) {
Some("staticlib") => "a",
_ => "rlib",
}
};
let output = if let Some(o) = output_file {
o
} else if let Some(ref dir) = out_dir {
let name = crate_name.as_deref().unwrap_or("unknown");
let suffix = extra_filename.as_deref().unwrap_or("");
NormalizedPath::new(dir)
.join(format!("lib{name}{suffix}.{primary_ext}"))
.to_string_lossy()
.into_owned()
} else {
let name = crate_name.as_deref().unwrap_or_else(|| {
std::path::Path::new(&source)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
});
format!("lib{name}.{primary_ext}")
};
ParsedInvocation::Cacheable(CacheableCompilation {
compiler: NormalizedPath::new(compiler),
family: CompilerFamily::Rustc,
source_file: NormalizedPath::new(source),
output_file: NormalizedPath::new(output),
original_args: Arc::from(args.to_vec()),
unknown_flags,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn args(s: &[&str]) -> Vec<String> {
s.iter().map(|x| x.to_string()).collect()
}
#[test]
fn basic_cacheable_compilation() {
let result = parse_invocation("clang++", &args(&["-c", "hello.cpp", "-o", "hello.o"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("hello.o"));
assert_eq!(c.family, CompilerFamily::Clang);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn no_c_flag_is_non_cacheable() {
let result = parse_invocation("gcc", &args(&["hello.cpp", "-o", "hello"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn preprocessing_only_non_cacheable() {
let result = parse_invocation("gcc", &args(&["-E", "hello.cpp"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn multi_file_split() {
let result = parse_invocation("gcc", &args(&["-c", "a.cpp", "b.cpp"]));
match result {
ParsedInvocation::MultiFile {
compilations,
source_indices,
..
} => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("a.cpp"));
assert_eq!(compilations[0].output_file, NormalizedPath::new("a.o"));
assert_eq!(compilations[1].source_file, NormalizedPath::new("b.cpp"));
assert_eq!(compilations[1].output_file, NormalizedPath::new("b.o"));
assert_eq!(source_indices, vec![1, 2]);
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn multi_file_with_flags() {
let result = parse_invocation(
"g++",
&args(&["-c", "-O2", "main.cpp", "-Wall", "util.cpp"]),
);
match result {
ParsedInvocation::MultiFile {
compilations,
original_args,
source_indices,
} => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("main.cpp"));
assert_eq!(compilations[1].source_file, NormalizedPath::new("util.cpp"));
assert!(original_args.contains(&"-O2".to_string()));
assert!(original_args.contains(&"-Wall".to_string()));
assert_eq!(source_indices, vec![2, 4]);
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn multi_file_mixed_extensions() {
let result = parse_invocation("gcc", &args(&["-c", "file1.c", "file2.cpp"]));
match result {
ParsedInvocation::MultiFile { compilations, .. } => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("file1.c"));
assert_eq!(
compilations[1].source_file,
NormalizedPath::new("file2.cpp")
);
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn stdin_non_cacheable() {
let result = parse_invocation("gcc", &args(&["-c", "-"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn default_output_name() {
let result = parse_invocation("gcc", &args(&["-c", "foo.cpp"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("foo.o"));
}
_ => panic!("expected cacheable"),
}
}
#[test]
fn original_args_preserved() {
let input = args(&["-c", "hello.cpp", "-O2", "-std=c++17", "-DNDEBUG", "-Wall"]);
let result = parse_invocation("clang++", &input);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(*c.original_args, *input);
}
_ => panic!("expected cacheable"),
}
}
#[test]
fn unknown_flags_preserved_in_original_args() {
let input = args(&[
"-c",
"hello.cpp",
"--deploy-dependencies",
"--custom-flag=value",
"-o",
"hello.o",
]);
let result = parse_invocation("clang++", &input);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(*c.original_args, *input);
assert_eq!(c.source_file, NormalizedPath::new("hello.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("hello.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn include_pch_flag_with_value() {
let result = parse_invocation(
"clang++",
&args(&["-c", "foo.cpp", "-include-pch", "pch.h.pch", "-o", "foo.o"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
assert!(c.original_args.contains(&"-include-pch".to_string()));
assert!(c.original_args.contains(&"pch.h.pch".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_generation_cpp_header_is_cacheable() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "-c", "pch.h", "-o", "pch.h.pch"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("pch.h"));
assert_eq!(c.output_file, NormalizedPath::new("pch.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_generation_c_header_is_cacheable() {
let result = parse_invocation(
"gcc",
&args(&["-x", "c-header", "-c", "stdafx.h", "-o", "stdafx.h.gch"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("stdafx.h"));
assert_eq!(c.output_file, NormalizedPath::new("stdafx.h.gch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_generation_without_c_flag_is_cacheable() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "FastLED.h", "-o", "FastLED.h.pch"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("FastLED.h"));
assert_eq!(c.output_file, NormalizedPath::new("FastLED.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_generation_c_header_without_c_flag_is_cacheable() {
let result = parse_invocation(
"gcc",
&args(&["-x", "c-header", "stdafx.h", "-o", "stdafx.h.gch"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("stdafx.h"));
assert_eq!(c.output_file, NormalizedPath::new("stdafx.h.gch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_generation_with_meson_flags_is_cacheable() {
let result = parse_invocation(
"ctc-clang++",
&args(&[
"-x",
"c++-header",
"FastLED.h",
"-o",
"FastLED.h.pch",
"-MD",
"-MF",
"FastLED.h.pch.d",
"-fPIC",
"-Iinclude",
"-Werror=invalid-pch",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("FastLED.h"));
assert_eq!(c.output_file, NormalizedPath::new("FastLED.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn header_without_x_flag_is_not_source() {
let result = parse_invocation("clang++", &args(&["-c", "pch.h"]));
assert!(
matches!(result, ParsedInvocation::NonCacheable { .. }),
"bare .h without -x header mode should be non-cacheable"
);
}
#[test]
fn x_flag_reset_disables_header_mode() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-header",
"pch.h",
"-x",
"c++",
"main.cpp",
"-c",
"-o",
"main.o",
]),
);
match result {
ParsedInvocation::MultiFile { compilations, .. } => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("pch.h"));
assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn x_cpp_after_header_is_normal_compilation() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-header",
"-x",
"c++",
"main.cpp",
"-c",
"-o",
"main.o",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("main.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("main.o"));
}
other => panic!("expected Cacheable, got: {other:?}"),
}
}
#[test]
fn sticky_header_mode_cpp_not_spuriously_pch() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-header",
"pch.h",
"-o",
"pch.h.pch",
"-x",
"c++",
"-c",
"main.cpp",
"-o",
"main.o",
]),
);
match &result {
ParsedInvocation::MultiFile { compilations, .. } => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("pch.h"));
assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn sticky_header_mode_non_source_not_captured_after_reset() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "pch.h", "-x", "c++", "-c", "main.cpp"]),
);
match &result {
ParsedInvocation::MultiFile { compilations, .. } => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("pch.h"));
assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn sticky_header_mode_no_c_flag_after_reset_is_non_cacheable() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "-x", "c++", "main.cpp", "-o", "main"]),
);
assert!(
matches!(result, ParsedInvocation::NonCacheable { .. }),
"after -x c++ reset, no -c should be non-cacheable, got: {result:?}"
);
}
#[test]
fn header_unit_c_is_cacheable() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c-header-unit", "foo.h", "-o", "foo.pcm"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn header_unit_cpp_is_cacheable() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header-unit", "foo.h", "-o", "foo.pcm"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn detect_clang_family() {
assert_eq!(detect_family("clang++"), CompilerFamily::Clang);
assert_eq!(detect_family("/usr/bin/clang"), CompilerFamily::Clang);
assert_eq!(detect_family("gcc"), CompilerFamily::Gcc);
assert_eq!(detect_family("g++"), CompilerFamily::Gcc);
}
#[test]
fn detect_emcc_family() {
assert_eq!(detect_family("emcc"), CompilerFamily::Clang);
assert_eq!(detect_family("em++"), CompilerFamily::Clang);
assert_eq!(detect_family("/usr/bin/emcc"), CompilerFamily::Clang);
assert_eq!(detect_family("emcc.exe"), CompilerFamily::Clang);
assert!(CompilerFamily::Clang.supports_depfile());
}
#[test]
fn detect_msvc_family() {
assert_eq!(detect_family("cl"), CompilerFamily::Msvc);
assert_eq!(detect_family("C:\\MSVC\\cl"), CompilerFamily::Msvc);
}
#[test]
fn detect_msvc_case_insensitive() {
assert_eq!(detect_family("CL"), CompilerFamily::Msvc);
assert_eq!(detect_family("CL.EXE"), CompilerFamily::Msvc);
assert_eq!(detect_family("Cl.exe"), CompilerFamily::Msvc);
assert_eq!(detect_family("C:\\MSVC\\CL.EXE"), CompilerFamily::Msvc);
assert_eq!(
detect_family("C:\\Program Files\\MSVC\\cl.EXE"),
CompilerFamily::Msvc
);
}
#[test]
fn gcc_supports_depfile() {
assert!(CompilerFamily::Gcc.supports_depfile());
}
#[test]
fn clang_supports_depfile() {
assert!(CompilerFamily::Clang.supports_depfile());
}
#[test]
fn msvc_no_depfile() {
assert!(!CompilerFamily::Msvc.supports_depfile());
}
#[test]
fn pch_default_output_clang() {
let result = parse_invocation("clang++", &args(&["-x", "c++-header", "src/pch.h"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("src/pch.h"));
assert_eq!(c.output_file, NormalizedPath::new("pch.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_default_output_gcc() {
let result = parse_invocation("gcc", &args(&["-x", "c-header", "src/pch.h"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("src/pch.h"));
assert_eq!(c.output_file, NormalizedPath::new("pch.h.gch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_default_output_strips_directory() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "src/fl/audio/fft/fft.h"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("src/fl/audio/fft/fft.h"));
assert_eq!(c.output_file, NormalizedPath::new("fft.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_default_output_absolute_path_strips_to_filename() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "/abs/path/src/pch.h"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("/abs/path/src/pch.h"));
assert_eq!(c.output_file, NormalizedPath::new("pch.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_default_output_explicit_o_unchanged() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "pch.h", "-o", "build/pch.h.pch"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("pch.h"));
assert_eq!(c.output_file, NormalizedPath::new("build/pch.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn normal_compile_default_output_unchanged() {
let result = parse_invocation("gcc", &args(&["-c", "foo.cpp"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("foo.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn concatenated_o_flag_parsed() {
let result = parse_invocation("clang", &args(&["-c", "foo.cpp", "-obuild/foo.o"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("build/foo.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn concatenated_o_flag_pch() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "pch.h", "-obuild/pch.h.pch"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("build/pch.h.pch"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn all_flags_preserved() {
let input = args(&[
"-c",
"foo.cpp",
"-o",
"foo.o",
"-Wall",
"-Wextra",
"-O2",
"-Xclang",
"-fno-spell-checking",
"-std=c++17",
"-DFOO=bar",
"-I/usr/include",
"-isystem",
"/usr/local/include",
"-unknown-future-flag",
]);
let result = parse_invocation("clang++", &input);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("foo.o"));
assert!(c.unknown_flags.contains(&"-Wall".to_string()));
assert!(c.unknown_flags.contains(&"-Wextra".to_string()));
assert!(c.unknown_flags.contains(&"-O2".to_string()));
assert!(c
.unknown_flags
.contains(&"-unknown-future-flag".to_string()));
assert!(c.unknown_flags.contains(&"-std=c++17".to_string()));
assert!(c.unknown_flags.contains(&"-DFOO=bar".to_string()));
assert!(c.unknown_flags.contains(&"-I/usr/include".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn xclang_value_not_misidentified_as_source() {
let result = parse_invocation(
"clang++",
&args(&[
"-c",
"foo.cpp",
"-Xclang",
"-fno-spell-checking",
"-o",
"foo.o",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn mllvm_value_not_misidentified_as_source() {
let result = parse_invocation(
"clang++",
&args(&["-c", "foo.cpp", "-mllvm", "-some-llvm-opt", "-o", "foo.o"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn pch_output_path_mismatch_repro() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header", "src/fl/fx/2d/flowfield_q31.h"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("flowfield_q31.h.pch"));
assert_eq!(
c.source_file,
NormalizedPath::new("src/fl/fx/2d/flowfield_q31.h")
);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn detect_rustc_family() {
assert_eq!(detect_family("rustc"), CompilerFamily::Rustc);
assert_eq!(detect_family("/usr/bin/rustc"), CompilerFamily::Rustc);
assert_eq!(detect_family("rustc.exe"), CompilerFamily::Rustc);
assert_eq!(
detect_family("C:\\rustup\\rustc.exe"),
CompilerFamily::Rustc
);
}
#[test]
fn rustc_no_depfile_support() {
assert!(!CompilerFamily::Rustc.supports_depfile());
}
#[test]
fn rustc_no_pch_extension() {
assert_eq!(CompilerFamily::Rustc.pch_extension(), None);
}
#[test]
fn rustc_lib_crate_is_cacheable() {
let result = parse_invocation(
"rustc",
&args(&[
"--edition",
"2021",
"--crate-type",
"lib",
"--emit=dep-info,metadata,link",
"-C",
"opt-level=2",
"src/lib.rs",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.family, CompilerFamily::Rustc);
assert_eq!(c.source_file, NormalizedPath::new("src/lib.rs"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn rustc_rlib_crate_is_cacheable() {
let result = parse_invocation("rustc", &args(&["--crate-type", "rlib", "src/lib.rs"]));
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_staticlib_crate_is_cacheable() {
let result = parse_invocation("rustc", &args(&["--crate-type", "staticlib", "src/lib.rs"]));
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_bin_crate_is_non_cacheable() {
let result = parse_invocation("rustc", &args(&["--crate-type", "bin", "src/main.rs"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn rustc_dylib_is_non_cacheable() {
let result = parse_invocation("rustc", &args(&["--crate-type", "dylib", "src/lib.rs"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn rustc_proc_macro_is_non_cacheable() {
let result = parse_invocation(
"rustc",
&args(&["--crate-type", "proc-macro", "src/lib.rs"]),
);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn rustc_cdylib_is_non_cacheable() {
let result = parse_invocation("rustc", &args(&["--crate-type", "cdylib", "src/lib.rs"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn rustc_no_crate_type_defaults_to_bin_non_cacheable() {
let result = parse_invocation("rustc", &args(&["src/main.rs"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn rustc_incremental_is_cacheable() {
let result = parse_invocation(
"rustc",
&args(&[
"--crate-type",
"lib",
"-C",
"incremental=/tmp/incr",
"src/lib.rs",
]),
);
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_no_source_is_non_cacheable() {
let result = parse_invocation("rustc", &args(&["--version"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn rustc_emit_metadata_is_cacheable() {
let result = parse_invocation(
"rustc",
&args(&["--crate-type", "lib", "--emit=metadata", "src/lib.rs"]),
);
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_output_with_explicit_o() {
let result = parse_invocation(
"rustc",
&args(&["--crate-type", "lib", "src/lib.rs", "-o", "libfoo.rlib"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("libfoo.rlib"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn rustc_metadata_only_output_is_rmeta() {
let result = parse_invocation(
"rustc",
&args(&[
"--crate-type",
"lib",
"--crate-name",
"mylib",
"--emit=dep-info,metadata",
"--out-dir",
"/target/debug/deps",
"-C",
"extra-filename=-abc123",
"src/lib.rs",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(
c.output_file,
NormalizedPath::new("/target/debug/deps/libmylib-abc123.rmeta")
);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn rustc_output_from_out_dir() {
let result = parse_invocation(
"rustc",
&args(&[
"--crate-type",
"lib",
"--crate-name",
"mylib",
"--out-dir",
"/target/debug/deps",
"-C",
"extra-filename=-abc123",
"src/lib.rs",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(
c.output_file,
NormalizedPath::new("/target/debug/deps/libmylib-abc123.rlib")
);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn rustc_full_cargo_invocation_cacheable() {
let result = parse_invocation(
"rustc",
&args(&[
"--edition",
"2021",
"--crate-type",
"lib",
"--crate-name",
"serde",
"--emit=dep-info,metadata,link",
"-C",
"opt-level=2",
"-C",
"metadata=abc123def",
"-C",
"extra-filename=-abc123def",
"--out-dir",
"/target/release/deps",
"-L",
"dependency=/target/release/deps",
"--extern",
"serde_derive=/target/release/deps/libserde_derive-xyz.so",
"--cap-lints",
"allow",
"--cfg",
"feature=\"derive\"",
"--cfg",
"feature=\"std\"",
"src/lib.rs",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.family, CompilerFamily::Rustc);
assert_eq!(c.source_file, NormalizedPath::new("src/lib.rs"));
assert_eq!(
c.output_file,
NormalizedPath::new("/target/release/deps/libserde-abc123def.rlib")
);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn rustc_original_args_preserved() {
let input = args(&["--edition", "2021", "--crate-type", "lib", "src/lib.rs"]);
let result = parse_invocation("rustc", &input);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(*c.original_args, *input);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn rustc_equal_form_crate_type() {
let result = parse_invocation("rustc", &args(&["--crate-type=lib", "src/lib.rs"]));
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_concatenated_c_incremental_is_cacheable() {
let result = parse_invocation(
"rustc",
&args(&["--crate-type", "lib", "-Cincremental=/tmp", "src/lib.rs"]),
);
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_comma_separated_crate_type_all_cacheable() {
let result = parse_invocation("rustc", &args(&["--crate-type", "lib,rlib", "src/lib.rs"]));
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_comma_separated_crate_type_mixed_non_cacheable() {
let result = parse_invocation("rustc", &args(&["--crate-type", "lib,dylib", "src/lib.rs"]));
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn rustc_comma_separated_crate_type_equals_form() {
let result = parse_invocation(
"rustc",
&args(&["--crate-type=lib,staticlib", "src/lib.rs"]),
);
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn rustc_test_flag_makes_non_cacheable() {
let result = parse_invocation(
"rustc",
&args(&["--crate-type", "lib", "--test", "src/lib.rs"]),
);
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn detect_clippy_driver_family() {
assert_eq!(detect_family("clippy-driver"), CompilerFamily::Rustc);
assert_eq!(
detect_family(
"/home/user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/clippy-driver"
),
CompilerFamily::Rustc
);
assert_eq!(
detect_family("C:\\Users\\user\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\bin\\clippy-driver.exe"),
CompilerFamily::Rustc
);
}
#[test]
fn detect_clippy_driver_versioned() {
assert_eq!(detect_family("clippy-driver-1.78"), CompilerFamily::Rustc);
}
#[test]
fn clippy_driver_cacheable_lib() {
let result = parse_invocation(
"clippy-driver",
&args(&[
"--crate-name",
"mycrate",
"--crate-type",
"lib",
"--emit=metadata,dep-info",
"--out-dir",
"target/debug/deps",
"-C",
"extra-filename=-abc123",
"src/lib.rs",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.family, CompilerFamily::Rustc);
assert_eq!(c.source_file, NormalizedPath::new("src/lib.rs"));
assert!(c.output_file.to_str().unwrap().ends_with(".rmeta"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn clippy_driver_non_cacheable_bin() {
let result = parse_invocation(
"clippy-driver",
&args(&[
"--crate-name",
"mybin",
"--crate-type",
"bin",
"src/main.rs",
]),
);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn clippy_driver_with_lint_flags() {
let result = parse_invocation(
"clippy-driver",
&args(&[
"--crate-name",
"mycrate",
"--crate-type",
"lib",
"-W",
"clippy::all",
"-D",
"clippy::unwrap_used",
"-A",
"clippy::too_many_arguments",
"src/lib.rs",
]),
);
assert!(matches!(result, ParsedInvocation::Cacheable(_)));
}
#[test]
fn cppm_extension_is_cacheable() {
let result = parse_invocation("clang++", &args(&["-c", "module.cppm", "-o", "module.pcm"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
assert_eq!(c.output_file, NormalizedPath::new("module.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn ixx_extension_is_cacheable() {
let result = parse_invocation("g++", &args(&["-c", "module.ixx", "-o", "module.o"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("module.ixx"));
assert_eq!(c.output_file, NormalizedPath::new("module.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn cppm_default_output_with_precompile_is_pcm() {
let result = parse_invocation("clang++", &args(&["--precompile", "module.cppm"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
assert_eq!(c.output_file, NormalizedPath::new("module.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn cppm_default_output_with_c_flag_is_object() {
let result = parse_invocation("clang++", &args(&["-c", "module.cppm"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
assert_eq!(c.output_file, NormalizedPath::new("module.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn cppm_multi_file() {
let result = parse_invocation("clang++", &args(&["-c", "a.cppm", "b.cppm"]));
match result {
ParsedInvocation::MultiFile { compilations, .. } => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("a.cppm"));
assert_eq!(compilations[1].source_file, NormalizedPath::new("b.cppm"));
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn x_cpp_module_with_precompile_is_cacheable() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-module",
"--precompile",
"interface.cpp",
"-o",
"interface.pcm",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_module_with_c_flag_is_cacheable() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-module",
"-c",
"interface.cpp",
"-o",
"interface.o",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("interface.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_module_without_c_or_precompile_is_non_cacheable() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-module", "interface.cpp", "-o", "interface"]),
);
assert!(
matches!(result, ParsedInvocation::NonCacheable { .. }),
"-x c++-module without -c or --precompile should be non-cacheable, got: {result:?}"
);
}
#[test]
fn x_cpp_module_accepts_non_source_extension() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-module", "--precompile", "interface.mpp"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("interface.mpp"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_module_default_output_precompile() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-module", "--precompile", "interface.cpp"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_module_default_output_c_flag() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-module", "-c", "interface.cpp"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("interface.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_module_reset_by_x_cpp() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-module",
"--precompile",
"interface.mpp",
"-x",
"c++",
"-c",
"main.cpp",
]),
);
match result {
ParsedInvocation::MultiFile { compilations, .. } => {
assert_eq!(compilations.len(), 2);
assert_eq!(
compilations[0].source_file,
NormalizedPath::new("interface.mpp")
);
assert_eq!(compilations[1].source_file, NormalizedPath::new("main.cpp"));
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn x_cpp_module_implies_compilation_with_precompile() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-module",
"--precompile",
"interface.cpp",
"-o",
"interface.pcm",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_header_unit_with_precompile_is_cacheable() {
let result = parse_invocation(
"clang++",
&args(&[
"-x",
"c++-header-unit",
"--precompile",
"foo.h",
"-o",
"foo.pcm",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_c_header_unit_with_c_flag_is_cacheable() {
let result = parse_invocation(
"gcc",
&args(&["-x", "c-header-unit", "-c", "foo.h", "-o", "foo.pcm"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_header_unit_default_output_is_pcm() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header-unit", "--precompile", "foo.h"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("foo.h.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn x_cpp_header_unit_implies_compilation() {
let result = parse_invocation(
"clang++",
&args(&["-x", "c++-header-unit", "foo.h", "-o", "foo.pcm"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.h"));
assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn precompile_on_normal_cpp_is_cacheable() {
let result = parse_invocation("clang++", &args(&["--precompile", "foo.cpp"]));
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("foo.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("foo.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn precompile_without_source_is_non_cacheable() {
let result = parse_invocation("clang++", &args(&["--precompile", "-O2"]));
assert!(
matches!(result, ParsedInvocation::NonCacheable { .. }),
"--precompile without source should be non-cacheable, got: {result:?}"
);
}
#[test]
fn precompile_and_c_flag_together() {
let result = parse_invocation(
"clang++",
&args(&["--precompile", "-c", "module.cppm", "-o", "module.pcm"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
assert_eq!(c.output_file, NormalizedPath::new("module.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn gcc_fmodules_ts_with_cppm_is_cacheable() {
let result = parse_invocation(
"g++",
&args(&["-fmodules-ts", "-c", "module.cppm", "-o", "module.o"]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("module.cppm"));
assert_eq!(c.output_file, NormalizedPath::new("module.o"));
assert!(c.unknown_flags.contains(&"-fmodules-ts".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn gcc_fmodules_ts_with_x_module_precompile() {
let result = parse_invocation(
"g++",
&args(&[
"-fmodules-ts",
"-x",
"c++-module",
"--precompile",
"interface.cpp",
]),
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("interface.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("interface.pcm"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
}