use regex::Regex;
use std::collections::HashSet;
use std::sync::OnceLock;
fn define_line_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"(?m)^[ \t]*#[ \t]*define[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]*(.*)$").unwrap()
})
}
fn is_empty_macro_body(rest: &str) -> bool {
let mut text = rest;
if let Some(idx) = text.find("//") {
text = &text[..idx];
}
let text = text.trim();
if text.is_empty() {
return true;
}
text.starts_with("/*") && text.ends_with("*/")
}
fn find_empty_object_macros(source: &str) -> HashSet<String> {
let mut names = HashSet::new();
for m in define_line_re().captures_iter(source) {
let name = &m[1];
let rest = &m[2];
if is_empty_macro_body(rest) {
names.insert(name.to_string());
}
}
names
}
fn preproc_directive_line_ranges(source: &str) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut pos = 0;
let mut continuing = false;
for line in source.split_inclusive('\n') {
let trimmed = line.trim_start();
let is_directive = trimmed.starts_with('#') || continuing;
if is_directive {
ranges.push((pos, pos + line.len()));
}
let content = line.strip_suffix('\n').unwrap_or(line);
let content = content.strip_suffix('\r').unwrap_or(content);
continuing = is_directive && content.trim_end().ends_with('\\');
pos += line.len();
}
ranges
}
fn blank_occurrences(source: &str, names: &HashSet<String>) -> String {
if names.is_empty() {
return source.to_string();
}
let directive_lines = preproc_directive_line_ranges(source);
let mut out: Vec<u8> = source.as_bytes().to_vec();
for name in names {
let re = Regex::new(&format!(r"\b{}\b", regex::escape(name))).unwrap();
for m in re.find_iter(source) {
let (start, end) = (m.start(), m.end());
let on_directive_line = directive_lines
.iter()
.any(|&(ls, le)| start >= ls && end <= le);
if on_directive_line {
continue;
}
for b in out.iter_mut().take(end).skip(start) {
*b = b' ';
}
}
}
String::from_utf8(out).unwrap_or_else(|_| source.to_string())
}
const KNOWN_CROSS_FILE_EMPTY_MACROS: &[&str] = &["deliberate_fall_through"];
pub fn blank_empty_object_macros(source: &str) -> String {
let mut names = find_empty_object_macros(source);
for &name in KNOWN_CROSS_FILE_EMPTY_MACROS {
if source.contains(name) {
names.insert(name.to_string());
}
}
blank_occurrences(source, &names)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blanks_empty_macro_before_declaration() {
let src =
"#ifndef RLAPI\n #define RLAPI // exported\n#endif\nRLAPI void f(void);\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(!out.contains("RLAPI void"));
assert!(out.contains(" void f(void);"));
assert!(out.contains("#define RLAPI"));
}
#[test]
fn leaves_non_empty_macros_alone() {
let src = "#define MAX_SIZE 32\nint arr[MAX_SIZE];\n";
let out = blank_empty_object_macros(src);
assert_eq!(out, src);
}
#[test]
fn leaves_function_like_macros_alone() {
let src = "#define TRACELOG(level, ...) (void)0\nTRACELOG(1, \"x\");\n";
let out = blank_empty_object_macros(src);
assert_eq!(out, src);
}
#[test]
fn function_like_macro_with_empty_body_not_blanked() {
let src = "#define UNUSED(x)\nvoid f(int y) { UNUSED(y); }\n";
let out = blank_empty_object_macros(src);
assert_eq!(out, src);
}
#[test]
fn header_guard_macro_never_blanked() {
let src = "#ifndef _MY_HEADER_H_\n#define _MY_HEADER_H_\n\nint x;\n\n#endif /* _MY_HEADER_H_ */\n";
let out = blank_empty_object_macros(src);
assert_eq!(out, src);
}
#[test]
fn blanks_known_cross_file_fallthrough_marker_with_no_local_define() {
let src = concat!(
"static void f(int len, unsigned char *z, unsigned long long v) {\n",
" switch (len) {\n",
" default: z[1] = (unsigned char)v;\n",
" /* no break */ deliberate_fall_through\n",
" case 1: z[0] = (unsigned char)v;\n",
" }\n",
"}\n",
);
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(!out.contains("deliberate_fall_through"));
assert!(out.contains("case 1:"));
}
#[test]
fn leaves_source_alone_when_cross_file_marker_absent() {
let src = "int x = 1;\nint y = 2;\n";
let out = blank_empty_object_macros(src);
assert_eq!(out, src);
}
#[test]
fn preserves_byte_length_and_positions() {
let src = "#define RLAPI\nRLAPI int x;\nint y = 1;\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
let pos_orig = src.find("int y = 1;").unwrap();
let pos_out = out.find("int y = 1;").unwrap();
assert_eq!(pos_orig, pos_out);
}
}