use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
const QUALIFIER_KEYWORDS: &[&str] = &["const", "volatile", "restrict", "_Atomic"];
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' ';
}
if let Some(semi) = terminating_semicolon_to_blank(source, start, end, &directive_lines)
{
out[semi] = b' ';
}
}
}
String::from_utf8(out).unwrap_or_else(|_| source.to_string())
}
fn terminating_semicolon_to_blank(
source: &str,
start: usize,
end: usize,
directive_lines: &[(usize, usize)],
) -> Option<usize> {
let bytes = source.as_bytes();
if source[..start]
.rsplit('\n')
.next()
.is_some_and(|head| !head.trim().is_empty())
{
return None;
}
let mut i = end;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if bytes.get(i) != Some(&b';') {
return None;
}
let mut j = start;
loop {
while j > 0 && bytes[j - 1].is_ascii_whitespace() {
j -= 1;
}
if j >= 2 && &source[j - 2..j] == "*/" {
match source[..j - 2].rfind("/*") {
Some(open) => {
j = open;
continue;
}
None => return None,
}
}
if let Some(&(ls, _)) = directive_lines
.iter()
.find(|&&(ls, le)| j > ls && j - 1 < le)
{
if ls == 0 {
return Some(i);
}
j = ls;
continue;
}
break;
}
let Some(&prev) = bytes.get(j.wrapping_sub(1)).filter(|_| j > 0) else {
return Some(i);
};
matches!(prev, b';' | b'{' | b'}').then_some(i)
}
const KNOWN_CROSS_FILE_EMPTY_MACROS: &[&str] = &["deliberate_fall_through"];
pub fn blank_empty_object_macros(source: &str) -> String {
let aliased = substitute_qualifier_alias_macros(source);
let mut names = find_empty_object_macros(&aliased);
for &name in KNOWN_CROSS_FILE_EMPTY_MACROS {
if aliased.contains(name) {
names.insert(name.to_string());
}
}
blank_occurrences(&aliased, &names)
}
fn find_qualifier_alias_macros(source: &str) -> HashMap<String, &'static str> {
let mut aliases = HashMap::new();
for m in define_line_re().captures_iter(source) {
let name = &m[1];
let body = m[2].trim();
let body = body.split("//").next().unwrap_or(body).trim();
let body = if let Some(idx) = body.rfind("/*") {
if body[idx..].ends_with("*/") {
body[..idx].trim()
} else {
body
}
} else {
body
};
for kw in QUALIFIER_KEYWORDS {
if body == *kw && name.len() >= kw.len() {
aliases.insert(name.to_string(), *kw);
break;
}
}
}
aliases
}
fn substitute_qualifier_alias_macros(source: &str) -> String {
let aliases = find_qualifier_alias_macros(source);
if aliases.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, keyword) in &aliases {
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;
}
let kw_bytes = keyword.as_bytes();
for (i, b) in out.iter_mut().enumerate().take(end).skip(start) {
*b = if i - start < kw_bytes.len() {
kw_bytes[i - start]
} else {
b' '
};
}
}
}
String::from_utf8(out).unwrap_or_else(|_| source.to_string())
}
#[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 blanks_the_semicolon_of_a_bare_macro_statement() {
let src = "#define wsdStatInit\nint f(void){\n wsdStatInit;\n return 1;\n}\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("\n \n"), "got {:?}", out);
}
#[test]
fn blanks_the_semicolon_across_an_intervening_directive_line() {
let src = "#define wsdAutoextInit\nint f(void){\n#endif\n wsdAutoextInit;\n}\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("\n \n"), "got {:?}", out);
}
#[test]
fn keeps_the_semicolon_of_a_trailing_decorator_macro() {
let src = "#define PACK\nstruct s {\n int x;\n} PACK;\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("} ;"), "got {:?}", out);
}
#[test]
fn keeps_the_semicolon_when_it_is_a_required_statement() {
let src = "#define EMPTY\nvoid f(int x){\n if (x) EMPTY;\n}\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("if (x) ;"), "got {:?}", out);
}
#[test]
fn keeps_the_semicolon_of_a_line_starting_required_statement() {
let src = "#define EMPTY\nvoid f(int x){\n if (x)\n EMPTY;\n}\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("\n ;\n"), "got {:?}", out);
}
#[test]
fn keeps_the_semicolon_after_a_label() {
let src = "#define EMPTY\nvoid f(void){\n done: EMPTY;\n}\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("done: ;"), "got {:?}", out);
}
#[test]
fn leaves_a_macro_used_as_a_value_and_its_semicolon_alone() {
let src = "#define EMPTY\nvoid f(int *p){\n *p = 1 EMPTY;\n}\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("*p = 1 ;"), "got {:?}", out);
}
#[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 substitutes_qualifier_alias_macro_at_declarator_position() {
let src = "#define CONST const\nint f(int *CONST p);\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("int f(int *const p);"), "got {:?}", out);
assert!(out.contains("#define CONST const\n"));
}
#[test]
fn pads_longer_qualifier_alias_name_with_trailing_spaces() {
let src = "#define _CONST const\nint f(int *_CONST p);\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("int f(int *const p);"), "got {:?}", out);
}
#[test]
fn handles_multiple_qualifier_aliases() {
let src = "#define CONST const\n#define VOLATILE volatile\nint f(int *CONST p, int *VOLATILE q);\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(
out.contains("int f(int *const p, int *volatile q);"),
"got {:?}",
out
);
}
#[test]
fn qualifier_alias_shorter_than_keyword_not_substituted() {
let src = "#define _A const\nint f(int *_A p);\n";
let out = blank_empty_object_macros(src);
assert_eq!(out, src);
}
#[test]
fn qualifier_alias_left_alone_on_directive_lines() {
let src = "#define CONST const\n#ifdef CONST\nint x;\n#endif\nint f(int *CONST p);\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("#ifdef CONST\n"), "got {:?}", out);
assert!(out.contains("int f(int *const p);"), "got {:?}", out);
}
#[test]
fn substitution_ignores_trailing_comment_on_define() {
let src = "#define CONST const /* Tcl compat */\nint f(int *CONST p);\n";
let out = blank_empty_object_macros(src);
assert_eq!(out.len(), src.len());
assert!(out.contains("int f(int *const p);"), "got {:?}", out);
}
#[test]
fn non_qualifier_body_not_substituted() {
let src = "#define MAX 100\nint arr[MAX];\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);
}
}