use std::sync::Arc;
use crate::core::NormalizedPath;
use super::{CacheableCompilation, CompilerFamily, ParsedInvocation};
const MSVC_SOURCE_EXTENSIONS: &[&str] = &[
"c", "cc", "cpp", "cxx", "c++", "cppm", "ixx", "m", "mm", "i", "ii", "s", "sx",
];
fn is_flag(arg: &str) -> bool {
arg.starts_with('/') || arg.starts_with('-')
}
fn flag_body(arg: &str) -> &str {
arg.strip_prefix('/')
.or_else(|| arg.strip_prefix('-'))
.unwrap_or(arg)
}
fn strip_flag<'a>(arg: &'a str, head: &str) -> Option<&'a str> {
if let Some(rest) = arg.strip_prefix('/') {
return rest.strip_prefix(head);
}
if let Some(rest) = arg.strip_prefix('-') {
return rest.strip_prefix(head);
}
None
}
fn is_exact_flag(arg: &str, head: &str) -> bool {
matches!(arg.strip_prefix('/'), Some(rest) if rest == head)
|| matches!(arg.strip_prefix('-'), Some(rest) if rest == head)
}
fn is_msvc_source_file(path: &str) -> bool {
if let Some(ext) = std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
{
let lower = ext.to_ascii_lowercase();
MSVC_SOURCE_EXTENSIONS.contains(&lower.as_str())
} else {
false
}
}
#[must_use]
pub fn looks_like_msvc_args(args: &[String]) -> bool {
args.iter().any(|arg| {
if !arg.starts_with('/') {
return false;
}
let rest = &arg[1..];
if rest.is_empty() {
return false;
}
let first = rest.chars().next().unwrap_or(' ');
if !first.is_ascii_alphabetic() {
return false;
}
if rest.contains('/') {
return false;
}
true
})
}
fn msvc_default_output(source: &str) -> String {
let stem = std::path::Path::new(source)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("a");
format!("{stem}.obj")
}
#[must_use]
pub fn parse_msvc_invocation(
compiler: &str,
args: &[String],
family: CompilerFamily,
) -> ParsedInvocation {
let mut has_c_flag = false;
let mut output_file: Option<String> = None;
let mut source_files: Vec<(String, usize)> = Vec::new();
let mut unknown_flags: Vec<String> = Vec::new();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg.is_empty() {
i += 1;
continue;
}
if is_exact_flag(arg, "E") || is_exact_flag(arg, "EP") || is_exact_flag(arg, "P") {
return ParsedInvocation::NonCacheable {
reason: format!("preprocessing-only flag: {arg}"),
};
}
if arg == "/?"
|| is_exact_flag(arg, "help")
|| is_exact_flag(arg, "HELP")
|| arg == "--version"
|| arg == "--help"
{
return ParsedInvocation::NonCacheable {
reason: format!("help/version query: {arg}"),
};
}
if is_exact_flag(arg, "c") {
has_c_flag = true;
i += 1;
continue;
}
if let Some(rest) = strip_flag(arg, "Fo") {
if rest.is_empty() {
if let Some(next) = args.get(i + 1) {
output_file = Some(next.clone());
i += 2;
continue;
}
i += 1;
continue;
}
let path = rest.strip_prefix(':').unwrap_or(rest);
output_file = Some(path.to_string());
i += 1;
continue;
}
if strip_flag(arg, "Fe").is_some() {
unknown_flags.push(arg.clone());
i += 1;
continue;
}
if strip_flag(arg, "Fd").is_some() {
unknown_flags.push(arg.clone());
i += 1;
continue;
}
if strip_flag(arg, "Fp").is_some() {
unknown_flags.push(arg.clone());
i += 1;
continue;
}
if arg == "-o" {
if let Some(next) = args.get(i + 1) {
output_file = Some(next.clone());
i += 2;
continue;
}
i += 1;
continue;
}
if let Some(path) = arg.strip_prefix("-o") {
if !path.is_empty() {
output_file = Some(path.to_string());
i += 1;
continue;
}
}
if let Some(rest) = strip_flag(arg, "Tc") {
if rest.is_empty() {
if let Some(next) = args.get(i + 1) {
source_files.push((next.clone(), i + 1));
i += 2;
continue;
}
i += 1;
continue;
}
source_files.push((rest.to_string(), i));
i += 1;
continue;
}
if let Some(rest) = strip_flag(arg, "Tp") {
if rest.is_empty() {
if let Some(next) = args.get(i + 1) {
source_files.push((next.clone(), i + 1));
i += 2;
continue;
}
i += 1;
continue;
}
source_files.push((rest.to_string(), i));
i += 1;
continue;
}
if is_exact_flag(arg, "TC") || is_exact_flag(arg, "TP") {
unknown_flags.push(arg.clone());
i += 1;
continue;
}
if is_exact_flag(arg, "D")
|| is_exact_flag(arg, "U")
|| is_exact_flag(arg, "I")
|| is_exact_flag(arg, "FI")
{
unknown_flags.push(arg.clone());
if let Some(next) = args.get(i + 1) {
unknown_flags.push(next.clone());
i += 2;
continue;
}
i += 1;
continue;
}
if is_flag(arg) {
let body = flag_body(arg);
let has_inner_slash = body.contains('/');
let looks_like_msvc_flag = !has_inner_slash
&& body
.chars()
.next()
.map(|c| c.is_ascii_alphabetic() || c == '?')
.unwrap_or(false);
if arg.starts_with('-') || looks_like_msvc_flag {
unknown_flags.push(arg.clone());
i += 1;
continue;
}
}
if is_msvc_source_file(arg) {
source_files.push((arg.clone(), i));
} else {
unknown_flags.push(arg.clone());
}
i += 1;
}
if !has_c_flag {
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 in MSVC/clang-cl invocation".to_string(),
};
}
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, _)| CacheableCompilation {
compiler: NormalizedPath::new(compiler),
family,
source_file: NormalizedPath::new(src),
output_file: NormalizedPath::new(msvc_default_output(src)),
original_args: Arc::clone(&shared_args),
unknown_flags: unknown_flags.clone(),
})
.collect();
return ParsedInvocation::MultiFile {
compilations,
original_args: shared_args,
source_indices,
};
}
let (source, _) = source_files.into_iter().next().unwrap();
let output = output_file.unwrap_or_else(|| msvc_default_output(&source));
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,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn args(s: &[&str]) -> Vec<String> {
s.iter().map(|x| x.to_string()).collect()
}
#[test]
fn looks_like_msvc_detects_slash_c() {
assert!(looks_like_msvc_args(&args(&["/c", "foo.c"])));
}
#[test]
fn looks_like_msvc_detects_fo() {
assert!(looks_like_msvc_args(&args(&["/Fo:out.obj", "foo.c"])));
}
#[test]
fn looks_like_msvc_rejects_unix_paths() {
assert!(!looks_like_msvc_args(&args(&[
"-c",
"/usr/include/foo.c",
"-o",
"foo.o"
])));
}
#[test]
fn looks_like_msvc_rejects_pure_gcc() {
assert!(!looks_like_msvc_args(&args(&[
"-c", "foo.c", "-o", "foo.o", "-O2", "-Wall"
])));
}
#[test]
fn looks_like_msvc_mixed_dash_and_slash() {
assert!(looks_like_msvc_args(&args(&["-DFOO=1", "/c", "foo.c"])));
}
#[test]
fn basic_clang_cl_compile_with_slash_fo_colon() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/Fo:hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.c"));
assert_eq!(c.output_file, NormalizedPath::new("hello.obj"));
assert_eq!(c.family, CompilerFamily::Msvc);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn slash_fo_concatenated_no_separator() {
let result = parse_msvc_invocation(
"cl",
&args(&["/c", "/Fohello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("hello.obj"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn slash_fo_space_separated() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/Fo", "build/hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("build/hello.obj"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn dash_fo_alias_accepted() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["-c", "-Fo:hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("hello.obj"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn gcc_style_dash_o_accepted_for_clang_cl() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["-c", "-o", "hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("hello.obj"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn default_output_when_no_fo() {
let result =
parse_msvc_invocation("clang-cl", &args(&["/c", "hello.c"]), CompilerFamily::Msvc);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("hello.obj"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn cpp_with_ehsc_and_std() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&[
"/c",
"/EHsc",
"/std:c++17",
"/MD",
"/W4",
"/Fo:hello.obj",
"hello.cpp",
]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.cpp"));
assert_eq!(c.output_file, NormalizedPath::new("hello.obj"));
assert!(c.unknown_flags.contains(&"/EHsc".to_string()));
assert!(c.unknown_flags.contains(&"/std:c++17".to_string()));
assert!(c.unknown_flags.contains(&"/MD".to_string()));
assert!(c.unknown_flags.contains(&"/W4".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn d_macro_with_value_space_separated() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/D", "FOO=1", "/Fo:hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.c"));
assert!(c.unknown_flags.contains(&"/D".to_string()));
assert!(c.unknown_flags.contains(&"FOO=1".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn i_path_with_space_and_spaces_in_path() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&[
"/c",
"/I",
"C:\\Program Files\\include",
"/Fo:hello.obj",
"hello.c",
]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.c"));
assert!(c.unknown_flags.contains(&"/I".to_string()));
assert!(c
.unknown_flags
.contains(&"C:\\Program Files\\include".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn mixed_dash_and_slash_flags() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "-DFOO=1", "/DBAR=2", "/Fo:hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.c"));
assert!(c.unknown_flags.contains(&"-DFOO=1".to_string()));
assert!(c.unknown_flags.contains(&"/DBAR=2".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn unknown_slash_flag_does_not_drop_invocation() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/XYZUnknown", "/Fo:hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert!(c.unknown_flags.contains(&"/XYZUnknown".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn debug_info_flags() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/Zi", "/Fd:vc.pdb", "/Fo:hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("hello.obj"));
assert!(c.unknown_flags.contains(&"/Zi".to_string()));
assert!(c.unknown_flags.contains(&"/Fd:vc.pdb".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn show_includes_kept() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/showIncludes", "/Fo:hello.obj", "hello.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert!(c.unknown_flags.contains(&"/showIncludes".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn slash_tc_concatenated_source() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/Tchello.c", "/Fo:hello.obj"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.c"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn slash_tp_space_separated_source() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/Tp", "hello.cpp", "/Fo:hello.obj"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(c.source_file, NormalizedPath::new("hello.cpp"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn multi_file_msvc_split() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "a.c", "b.c"]),
CompilerFamily::Msvc,
);
match result {
ParsedInvocation::MultiFile {
compilations,
source_indices,
..
} => {
assert_eq!(compilations.len(), 2);
assert_eq!(compilations[0].source_file, NormalizedPath::new("a.c"));
assert_eq!(compilations[0].output_file, NormalizedPath::new("a.obj"));
assert_eq!(compilations[1].source_file, NormalizedPath::new("b.c"));
assert_eq!(compilations[1].output_file, NormalizedPath::new("b.obj"));
assert_eq!(source_indices, vec![1, 2]);
}
other => panic!("expected MultiFile, got: {other:?}"),
}
}
#[test]
fn no_slash_c_is_non_cacheable() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["hello.c", "/Fe:hello.exe"]),
CompilerFamily::Msvc,
);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn slash_e_preprocess_only_is_non_cacheable() {
let result =
parse_msvc_invocation("clang-cl", &args(&["/E", "hello.c"]), CompilerFamily::Msvc);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn slash_p_preprocess_to_file_is_non_cacheable() {
let result =
parse_msvc_invocation("clang-cl", &args(&["/P", "hello.c"]), CompilerFamily::Msvc);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn slash_help_is_non_cacheable() {
let result = parse_msvc_invocation("clang-cl", &args(&["/?"]), CompilerFamily::Msvc);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn dash_dash_version_is_non_cacheable() {
let result = parse_msvc_invocation("clang-cl", &args(&["--version"]), CompilerFamily::Msvc);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn no_source_file_is_non_cacheable() {
let result = parse_msvc_invocation(
"clang-cl",
&args(&["/c", "/Fo:foo.obj"]),
CompilerFamily::Msvc,
);
assert!(matches!(result, ParsedInvocation::NonCacheable { .. }));
}
#[test]
fn original_args_preserved_for_compiler_fallback() {
let input = args(&[
"/c",
"/EHsc",
"/std:c++17",
"/Fo:hello.obj",
"/DDEBUG=1",
"hello.cpp",
]);
let result = parse_msvc_invocation("clang-cl", &input, CompilerFamily::Msvc);
match result {
ParsedInvocation::Cacheable(c) => {
assert_eq!(*c.original_args, *input);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
}