pub const PATH_SENTINEL: &str = "<path>";
pub fn extract_cc1_line(stderr: &str) -> Option<&str> {
stderr
.lines()
.map(str::trim)
.find(|line| line.contains("\"-cc1\""))
}
fn tokenize(line: &str) -> Vec<String> {
let mut out = Vec::new();
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c != '"' {
continue;
}
let mut tok = String::new();
while let Some(c) = chars.next() {
match c {
'"' => break,
'\\' => match chars.peek() {
Some('"') | Some('\\') => tok.push(chars.next().unwrap()),
_ => tok.push('\\'),
},
_ => tok.push(c),
}
}
out.push(tok);
}
out
}
fn is_abs_path_start(s: &str, windows_aware: bool) -> bool {
if s.starts_with('/') {
return true;
}
if !windows_aware {
return false;
}
let b = s.as_bytes();
if s.starts_with("\\\\") {
return true;
}
b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && (b[2] == b'\\' || b[2] == b'/')
}
fn sentinel_token(tok: &str, windows_aware: bool) -> String {
if is_abs_path_start(tok, windows_aware) {
return PATH_SENTINEL.to_string();
}
if let Some(eq) = tok.find('=') {
let (head, val) = tok.split_at(eq + 1);
if is_abs_path_start(val, windows_aware) {
return format!("{head}{PATH_SENTINEL}");
}
}
if let Some(rest) = tok.strip_prefix("-I")
&& is_abs_path_start(rest, windows_aware)
{
return format!("-I{PATH_SENTINEL}");
}
tok.to_string()
}
pub fn semantic_tokens(
cc1_line: &str,
windows_aware: bool,
per_tu_paths: &[String],
) -> Vec<String> {
let toks = tokenize(cc1_line);
if toks.is_empty() {
return Vec::new();
}
let last = toks.len() - 1;
let mut out = Vec::with_capacity(toks.len());
let mut i = 0;
while i < toks.len() {
let tok = &toks[i];
if i != last
&& let Some(skip) = dependency_flag_skip(tok, false)
{
i += skip;
continue;
}
if i == last {
out.push(PATH_SENTINEL.to_string());
} else if let Some(head) = per_tu_path_head(tok, per_tu_paths) {
out.push(format!("{head}{PATH_SENTINEL}"));
} else {
out.push(sentinel_token(tok, windows_aware));
}
i += 1;
}
out
}
fn per_tu_path_head<'a>(tok: &'a str, per_tu_paths: &[String]) -> Option<&'a str> {
if per_tu_paths.iter().any(|p| p == tok) {
return Some("");
}
if let Some(eq) = tok.find('=') {
let (head, val) = tok.split_at(eq + 1);
if per_tu_paths.iter().any(|p| p == val) {
return Some(head);
}
}
None
}
pub fn extract_gnu_cc1_line(stderr: &str) -> Option<&str> {
stderr.lines().map(str::trim).find(|line| {
let first = line.split_whitespace().next().unwrap_or("");
let base = first.rsplit(['/', '\\']).next().unwrap_or(first);
base == "cc1" || base == "cc1plus"
})
}
fn tokenize_gnu(line: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_tok = false;
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c.is_whitespace() {
if in_tok {
out.push(std::mem::take(&mut cur));
in_tok = false;
}
continue;
}
in_tok = true;
if c == '"' {
while let Some(c) = chars.next() {
match c {
'"' => break,
'\\' => match chars.peek() {
Some('"') | Some('\\') => cur.push(chars.next().unwrap()),
_ => cur.push('\\'),
},
_ => cur.push(c),
}
}
} else {
cur.push(c);
}
}
if in_tok {
out.push(cur);
}
out
}
const GNU_DROP_WITH_VALUE: &[&str] = &[
"-o",
"-dumpdir",
"-dumpbase",
"-dumpbase-ext",
"-auxbase",
"-auxbase-strip",
];
#[derive(Clone, Copy)]
enum DepFlagArity {
Valueless,
WithValue,
}
fn dependency_flag_arity(tok: &str, gnu_cc1: bool) -> Option<DepFlagArity> {
use DepFlagArity::{Valueless, WithValue};
match tok {
"-M" | "-MM" | "-MG" | "-MP" | "-sys-header-deps" | "-module-file-deps" => Some(Valueless),
"-MF" | "-MT" | "-MQ" | "-dependency-file" | "-dependency-dot" => Some(WithValue),
"-MD" | "-MMD" if gnu_cc1 => Some(WithValue),
_ => None,
}
}
fn dependency_flag_skip(tok: &str, gnu_cc1: bool) -> Option<usize> {
dependency_flag_arity(tok, gnu_cc1).map(|arity| match arity {
DepFlagArity::Valueless => 1,
DepFlagArity::WithValue => 2,
})
}
fn gnu_semantic_tokens(
cc1_line: &str,
windows_aware: bool,
per_tu_paths: &[String],
) -> Vec<String> {
let toks = tokenize_gnu(cc1_line);
let mut out = Vec::new();
let mut i = 0;
if i < toks.len() {
i += 1;
}
while i < toks.len() {
let tok = &toks[i];
if tok == "-quiet" {
i += 1;
continue;
}
if GNU_DROP_WITH_VALUE.contains(&tok.as_str()) {
i += 2; continue;
}
if let Some(skip) = dependency_flag_skip(tok, true) {
i += skip;
continue;
}
if let Some(head) = per_tu_path_head(tok, per_tu_paths) {
out.push(format!("{head}{PATH_SENTINEL}"));
} else {
out.push(sentinel_token(tok, windows_aware));
}
i += 1;
}
out
}
pub fn resolved_semantic_tokens(
stderr: &str,
windows_aware: bool,
per_tu_paths: &[String],
) -> Option<Vec<String>> {
if let Some(line) = extract_cc1_line(stderr) {
return Some(semantic_tokens(line, windows_aware, per_tu_paths));
}
extract_gnu_cc1_line(stderr).map(|line| gnu_semantic_tokens(line, windows_aware, per_tu_paths))
}
#[cfg(test)]
mod tests {
use super::*;
const O2: &str = include_str!("testdata/clang_o2.txt");
const O2_NATIVE: &str = include_str!("testdata/clang_o2_march_native.txt");
const CL_WIN: &str = include_str!("testdata/clang_cl_windows.txt");
const GCC_O2: &str = include_str!("testdata/gcc_o2.txt");
const GCC_O2_NATIVE: &str = include_str!("testdata/gcc_o2_march_native.txt");
#[test]
fn extract_cc1_line_finds_the_compile_line_past_the_banner() {
let line = extract_cc1_line(O2).expect("fixture has a -cc1 line");
assert!(line.contains("\"-cc1\""));
assert!(line.contains("\"-O2\""));
}
#[test]
fn extract_cc1_line_is_none_without_a_compile_line() {
assert!(extract_cc1_line("clang version 1\nThread model: posix").is_none());
assert!(extract_cc1_line("").is_none());
}
#[test]
fn tokenize_unquotes_every_argument() {
let toks = tokenize(r#" "/usr/bin/clang" "-cc1" "-O2" "#);
assert_eq!(toks, vec!["/usr/bin/clang", "-cc1", "-O2"]);
}
#[test]
fn tokenize_handles_escaped_quote_and_backslash() {
let toks = tokenize(r#""a\"b" "c\\d""#);
assert_eq!(toks, vec!["a\"b", "c\\d"]);
}
#[test]
fn sentinel_token_blanks_posix_paths_where_one_can_begin() {
for wa in [false, true] {
assert_eq!(
sentinel_token("/Applications/Xcode/clang", wa),
PATH_SENTINEL
);
assert_eq!(
sentinel_token("-fdebug-compilation-dir=/Users/x/proj", wa),
format!("-fdebug-compilation-dir={PATH_SENTINEL}")
);
assert_eq!(
sentinel_token("-I/usr/local/include", wa),
format!("-I{PATH_SENTINEL}")
);
}
}
#[test]
fn sentinel_token_blanks_windows_paths_only_when_windows_aware() {
let cases = [
(
"C:\\Program Files\\LLVM\\bin\\clang-cl.exe",
PATH_SENTINEL.to_string(),
),
("c:/users/x/proj/a.c", PATH_SENTINEL.to_string()),
("\\\\server\\share\\inc", PATH_SENTINEL.to_string()),
("\\\\?\\C:\\Windows Kits\\10", PATH_SENTINEL.to_string()),
(
"-fdebug-compilation-dir=C:\\t\\proj",
format!("-fdebug-compilation-dir={PATH_SENTINEL}"),
),
("-IC:\\sdk\\include", format!("-I{PATH_SENTINEL}")),
];
for (tok, blanked) in cases {
assert_eq!(
sentinel_token(tok, true),
blanked,
"windows_aware must blank {tok}"
);
assert_eq!(
sentinel_token(tok, false),
tok,
"clang-cl (not windows_aware) must keep {tok} raw"
);
}
}
#[test]
fn sentinel_token_leaves_codegen_tokens_untouched() {
for wa in [false, true] {
for tok in [
"-cc1",
"-O2",
"-target-cpu",
"apple-m1",
"x86-64",
"+neon",
"-ffp-contract=on",
"-fms-compatibility-version=19.44.35221",
"-mrelocation-model",
"pic",
"C:", "C:rel", "-DNAME=1",
] {
assert_eq!(
sentinel_token(tok, wa),
tok,
"must not alter {tok} (wa={wa})"
);
}
}
}
#[test]
fn semantic_tokens_strips_every_host_local_path() {
let toks = resolved_semantic_tokens(O2, true, &[]).expect("fixture resolves");
for tok in &toks {
assert!(
!tok.starts_with('/'),
"absolute path leaked into the key: {tok}"
);
}
assert_eq!(toks.last().map(String::as_str), Some(PATH_SENTINEL));
assert!(toks.iter().any(|t| t == "-O2"));
assert!(toks.iter().any(|t| t == "-target-cpu"));
assert!(toks.iter().any(|t| t == "apple-m1"));
}
fn has_windows_path(tok: &str) -> bool {
let drive = |s: &str| {
let b = s.as_bytes();
b.len() >= 3
&& b[0].is_ascii_alphabetic()
&& b[1] == b':'
&& (b[2] == b'\\' || b[2] == b'/')
};
tok.contains("\\\\")
|| tok.split('=').any(drive)
|| drive(tok)
|| drive(tok.trim_start_matches("-I"))
}
#[test]
fn semantic_tokens_strips_windows_paths_for_gnu_clang() {
let toks = resolved_semantic_tokens(CL_WIN, true, &[]).expect("cl fixture resolves");
for tok in &toks {
assert!(
!has_windows_path(tok),
"Windows path leaked into the key: {tok}"
);
}
assert!(toks.iter().any(|t| t == "-gcodeview"));
assert!(toks.iter().any(|t| t == "-debug-info-kind=constructor"));
assert!(toks.iter().any(|t| t == "-fdebug-compilation-dir=<path>"));
}
#[test]
fn semantic_tokens_keeps_windows_paths_for_clang_cl() {
let toks = resolved_semantic_tokens(CL_WIN, false, &[]).expect("cl fixture resolves");
assert!(
toks.iter().any(|t| has_windows_path(t)),
"clang-cl tokens must keep raw Windows paths, none found"
);
let gnu = resolved_semantic_tokens(CL_WIN, true, &[]).unwrap();
assert_ne!(toks, gnu, "windows_aware must change the cl token list");
}
#[test]
fn semantic_tokens_is_deterministic() {
assert_eq!(
semantic_tokens(O2, true, &[]),
semantic_tokens(O2, true, &[])
);
}
#[test]
fn march_native_resolves_to_a_different_key_than_plain() {
let plain = resolved_semantic_tokens(O2, true, &[]).unwrap();
let native = resolved_semantic_tokens(O2_NATIVE, true, &[]).unwrap();
assert_ne!(
plain, native,
"-march=native must change the resolved invocation"
);
}
#[test]
fn per_tu_paths_are_blanked_from_resolved_tokens() {
let line_a = r#" "clang" "-cc1" "-O2" "-main-file-name" "a.c" "-o" "build/a.o" "x.c/a.c" "#;
let line_b = r#" "clang" "-cc1" "-O2" "-main-file-name" "b.c" "-o" "build/b.o" "x.c/b.c" "#;
let per_a = [
"x.c/a.c".to_string(),
"a.c".to_string(),
"build/a.o".to_string(),
];
let per_b = [
"x.c/b.c".to_string(),
"b.c".to_string(),
"build/b.o".to_string(),
];
let toks_a = semantic_tokens(line_a, true, &per_a);
let toks_b = semantic_tokens(line_b, true, &per_b);
assert_eq!(
toks_a, toks_b,
"per-TU paths must blank to an invariant token list"
);
assert!(toks_a.iter().any(|t| t == "-O2"));
assert!(
!toks_a.iter().any(|t| t == "a.c" || t == "build/a.o"),
"per-TU paths leaked: {toks_a:?}"
);
}
#[test]
fn dependency_file_flags_are_dropped_from_clang_tokens() {
let line_a = r#" "clang" "-cc1" "-O2" "-dependency-file" "build/a.c.d" "-MT" "build/a.c.o" "-c" "x/a.c" "#;
let line_b = r#" "clang" "-cc1" "-O2" "-dependency-file" "build/neighbour.c.d" "-MT" "build/b.c.o" "-c" "x/a.c" "#;
let toks_a = semantic_tokens(line_a, true, &[]);
let toks_b = semantic_tokens(line_b, true, &[]);
assert_eq!(
toks_a, toks_b,
"depfile path/target must not enter the key: {toks_a:?} vs {toks_b:?}"
);
for gone in [
"-dependency-file",
"-MT",
"build/a.c.d",
"build/neighbour.c.d",
"build/a.c.o",
] {
assert!(
!toks_a.iter().any(|t| t == gone),
"{gone} must be dropped: {toks_a:?}"
);
}
assert!(toks_a.iter().any(|t| t == "-O2"), "codegen flag dropped");
}
#[test]
fn dependency_file_flags_are_dropped_from_gnu_tokens() {
let line_a = r#" /usr/lib/gcc/cc1plus -O2 -MD build/a.c.d -MF build/a.c.d -MT build/a.c.o -MP -mabi=lp64 a.c "#;
let line_b = r#" /usr/lib/gcc/cc1plus -O2 -MD build/neighbour.c.d -MF build/b.c.d -MT build/b.c.o -MP -mabi=lp64 a.c "#;
let toks_a = gnu_semantic_tokens(line_a, true, &[]);
let toks_b = gnu_semantic_tokens(line_b, true, &[]);
assert_eq!(
toks_a, toks_b,
"gnu depfile path/target must not enter the key: {toks_a:?} vs {toks_b:?}"
);
for gone in [
"-MD",
"-MF",
"-MT",
"-MP",
"build/a.c.d",
"build/neighbour.c.d",
] {
assert!(
!toks_a.iter().any(|t| t == gone),
"{gone} must be dropped: {toks_a:?}"
);
}
assert!(toks_a.iter().any(|t| t == "-O2"));
assert!(toks_a.iter().any(|t| t == "-mabi=lp64"));
}
#[test]
fn semantic_tokens_drops_clang_dependency_sidecar_flags() {
let line = r#" "/usr/bin/clang" "-cc1" "-O2" "-dependency-file" "build/other-tu.d" "-MT" "obj/other-tu.o" "-MQ" "obj quoted.o" "-dependency-dot" "deps/other-tu.dot" "-MP" "-MG" "-sys-header-deps" "-module-file-deps" "-O3" "src/this-tu.c" "#;
let toks = semantic_tokens(line, true, &[]);
for dropped in [
"-dependency-file",
"build/other-tu.d",
"-MT",
"obj/other-tu.o",
"-MQ",
"obj quoted.o",
"-dependency-dot",
"deps/other-tu.dot",
"-MP",
"-MG",
"-sys-header-deps",
"-module-file-deps",
] {
assert!(
!toks.iter().any(|t| t == dropped),
"clang dep token leaked into key: {dropped} in {toks:?}"
);
}
assert!(toks.iter().any(|t| t == "-O2"));
assert!(
toks.iter().any(|t| t == "-O3"),
"valueless dep flags must not consume the following codegen token: {toks:?}"
);
assert_eq!(toks.last().map(String::as_str), Some(PATH_SENTINEL));
}
#[test]
fn gnu_semantic_tokens_drops_dependency_sidecar_flags() {
let line = " /usr/lib/gcc/cc1 -quiet -O2 -MD build/other-tu.d -MMD build/other-tu.mmd -MF deps/other-tu.d -MT obj/other-tu.o -MQ obj/quoted.o -MP -MG -M -O3 -MM -fno-semantic-interposition src/this-tu.c -o /tmp/ccXYZ.s";
let toks = gnu_semantic_tokens(line, true, &[]);
for dropped in [
"-MD",
"build/other-tu.d",
"-MMD",
"build/other-tu.mmd",
"-MF",
"deps/other-tu.d",
"-MT",
"obj/other-tu.o",
"-MQ",
"obj/quoted.o",
"-MP",
"-MG",
"-M",
"-MM",
] {
assert!(
!toks.iter().any(|t| t == dropped),
"gnu dep token leaked into key: {dropped} in {toks:?}"
);
}
assert!(toks.iter().any(|t| t == "-O2"));
assert!(
toks.iter().any(|t| t == "-O3"),
"valueless -M must not consume the following token: {toks:?}"
);
assert!(
toks.iter().any(|t| t == "-fno-semantic-interposition"),
"valueless -MM must not consume the following token: {toks:?}"
);
}
#[test]
fn extract_gnu_cc1_line_finds_the_cc1plus_subprocess() {
assert!(extract_cc1_line(GCC_O2).is_none());
let line = extract_gnu_cc1_line(GCC_O2).expect("fixture has a cc1plus line");
assert!(line.contains("cc1plus"));
assert!(line.contains("-O2"));
assert!(extract_gnu_cc1_line(O2).is_none());
}
#[test]
fn tokenize_gnu_handles_mixed_quoted_and_unquoted() {
let toks = tokenize_gnu(r#" /usr/lib/gcc/cc1plus -O2 "-mabi=lp64" -D_GNU_SOURCE "#);
assert_eq!(
toks,
vec!["/usr/lib/gcc/cc1plus", "-O2", "-mabi=lp64", "-D_GNU_SOURCE"]
);
}
#[test]
fn gnu_semantic_tokens_strip_paths_temp_and_output_management() {
let toks = resolved_semantic_tokens(GCC_O2, true, &[]).expect("gcc fixture resolves");
for tok in &toks {
assert!(!tok.starts_with('/'), "absolute path leaked: {tok}");
}
for dropped in ["-o", "-dumpdir", "-dumpbase", "-dumpbase-ext", "-quiet"] {
assert!(
!toks.iter().any(|t| t == dropped),
"{dropped} must be dropped from gnu tokens: {toks:?}"
);
}
assert!(
!toks.iter().any(|t| t.ends_with(".s")),
"temp asm path leaked: {toks:?}"
);
assert!(toks.iter().any(|t| t == "-O2"));
assert!(toks.iter().any(|t| t == "-fno-semantic-interposition"));
assert!(toks.iter().any(|t| t == "-mabi=lp64"));
}
#[test]
fn gnu_tokens_are_deterministic_despite_random_temp() {
let a = resolved_semantic_tokens(GCC_O2, true, &[]).unwrap();
let b = resolved_semantic_tokens(GCC_O2, true, &[]).unwrap();
assert_eq!(a, b);
}
#[test]
fn gnu_march_native_resolves_to_a_different_key_than_plain() {
let plain = resolved_semantic_tokens(GCC_O2, true, &[]).unwrap();
let native = resolved_semantic_tokens(GCC_O2_NATIVE, true, &[]).unwrap();
assert_ne!(plain, native, "-march=native must change gnu tokens");
}
#[test]
fn per_tu_path_head_matches_whole_and_attached_forms() {
let per = ["u00.c".to_string(), "build/u00.o".to_string()];
assert_eq!(per_tu_path_head("u00.c", &per), Some(""));
assert_eq!(
per_tu_path_head("-object-file-name=build/u00.o", &per),
Some("-object-file-name=")
);
assert_eq!(per_tu_path_head("-O2", &per), None);
assert_eq!(per_tu_path_head("u01.c", &per), None);
}
}