fn is_directive_start(trimmed: &str) -> bool {
trimmed.starts_with("#if") }
fn is_endif(trimmed: &str) -> bool {
trimmed.starts_with("#endif")
}
fn is_branch_directive(trimmed: &str) -> bool {
trimmed.starts_with("#else") || trimmed.starts_with("#elif")
}
fn is_bare_label_line(line: &str) -> bool {
let trimmed = line.trim();
let Some(name) = trimmed.strip_suffix(':') else {
return false;
};
if name.is_empty() || name == "default" {
return false;
}
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn blank_line(out: &mut [u8], line_start: usize, line_len: usize) {
for b in out.iter_mut().skip(line_start).take(line_len) {
if *b != b'\n' && *b != b'\r' {
*b = b' ';
}
}
}
fn directive_keyword_and_name(trimmed: &str) -> Option<(&'static str, &str)> {
let (keyword, after) = if let Some(rest) = trimmed.strip_prefix("#ifdef") {
("#ifdef", rest)
} else if let Some(rest) = trimmed.strip_prefix("#ifndef") {
("#ifndef", rest)
} else {
return None;
};
let name = after.split_whitespace().next()?;
Some((keyword, name))
}
fn open_marker(keyword: &str, name: &str) -> Option<String> {
let sigil = match keyword {
"#ifdef" => 'd',
"#ifndef" => 'n',
_ => return None,
};
Some(format!("/*G{sigil}:{name}*/"))
}
const CLOSE_MARKER: &str = "/*E*/";
fn write_marker_or_blank(out: &mut [u8], line_start: usize, line_len: usize, marker: Option<&str>) {
if let Some(marker) = marker {
if marker.len() <= line_len {
let region = &mut out[line_start..line_start + line_len];
region[..marker.len()].copy_from_slice(marker.as_bytes());
for b in &mut region[marker.len()..] {
*b = b' ';
}
return;
}
}
blank_line(out, line_start, line_len);
}
pub fn blank_label_guarded_preproc(source: &str) -> String {
let lines: Vec<&str> = source.lines().collect();
let mut line_starts = Vec::with_capacity(lines.len());
let mut offset = 0usize;
for line in &lines {
line_starts.push(offset);
offset += line.len() + 1; }
let mut out = source.as_bytes().to_vec();
let mut i = 0usize;
while i < lines.len() {
let trimmed = lines[i].trim_start();
if !is_directive_start(trimmed) {
i += 1;
continue;
}
let prev_content = (0..i).rev().find(|&k| !lines[k].trim().is_empty());
let follows_label = prev_content.is_some_and(|k| is_bare_label_line(lines[k]));
if !follows_label {
i += 1;
continue;
}
let mut depth = 1i32;
let mut end_idx = None;
let mut has_branch = false;
let mut j = i + 1;
while j < lines.len() {
let t = lines[j].trim_start();
if is_directive_start(t) {
depth += 1;
} else if is_endif(t) {
depth -= 1;
if depth == 0 {
end_idx = Some(j);
break;
}
} else if depth == 1 && is_branch_directive(t) {
has_branch = true;
}
j += 1;
}
let Some(end_idx) = end_idx else {
i += 1;
continue;
};
if !has_branch {
let open_text = directive_keyword_and_name(trimmed)
.and_then(|(keyword, name)| open_marker(keyword, name))
.filter(|m| m.len() <= lines[i].len());
write_marker_or_blank(
&mut out,
line_starts[i],
lines[i].len(),
open_text.as_deref(),
);
write_marker_or_blank(
&mut out,
line_starts[end_idx],
lines[end_idx].len(),
open_text.as_ref().map(|_| CLOSE_MARKER),
);
}
i += 1;
}
String::from_utf8(out).unwrap_or_else(|_| source.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::c_language;
fn parses_clean(src: &str) -> bool {
let fixed = blank_label_guarded_preproc(src);
let mut parser = tree_sitter::Parser::new();
parser.set_language(&c_language()).unwrap();
let tree = parser.parse(&fixed, None).unwrap();
!tree.root_node().has_error()
}
#[test]
fn fixes_label_immediately_followed_by_ifdef() {
let src = "\
void f(void) {
goto out;
out:
#ifdef X
os_free(rfds);
os_free(wfds);
os_free(efds);
#endif
return;
}
";
assert!(parses_clean(src));
}
#[test]
fn preserves_byte_length_and_line_count() {
let src = "\
void f(void) {
goto out;
out:
#ifdef X
os_free(rfds);
#endif
return;
}
";
let fixed = blank_label_guarded_preproc(src);
assert_eq!(fixed.len(), src.len());
assert_eq!(fixed.matches('\n').count(), src.matches('\n').count());
let pos_orig = src.find("return;").unwrap();
let pos_fixed = fixed.find("return;").unwrap();
assert_eq!(pos_orig, pos_fixed);
}
#[test]
fn every_statement_visible_as_identifier_after_fix() {
let src = "\
void f(void) {
goto out;
out:
#ifdef X
os_free(rfds);
os_free(wfds);
os_free(efds);
#endif
return;
}
";
let fixed = blank_label_guarded_preproc(src);
let mut parser = tree_sitter::Parser::new();
parser.set_language(&c_language()).unwrap();
let tree = parser.parse(&fixed, None).unwrap();
assert!(!tree.root_node().has_error());
let mut cursor = tree.root_node().walk();
let mut found_ifdef = false;
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
if node.kind() == "preproc_ifdef" {
found_ifdef = true;
}
for child in node.children(&mut cursor) {
stack.push(child);
}
}
assert!(!found_ifdef);
}
#[test]
fn leaves_ordinary_ifdef_block_untouched() {
let src = "\
int f(void) {
int x = 0;
#ifdef DEBUG_MODE
x = 1;
#endif
return x;
}
";
assert_eq!(blank_label_guarded_preproc(src), src);
}
#[test]
fn leaves_default_case_label_untouched() {
let src = "\
int f(int x) {
switch (x) {
default:
#ifdef Y
return 1;
#endif
return 0;
}
}
";
assert_eq!(blank_label_guarded_preproc(src), src);
}
#[test]
fn skips_block_with_its_own_else_branch() {
let src = "\
void f(void) {
goto out;
out:
#ifdef X
os_free(rfds);
#else
noop();
#endif
return;
}
";
assert_eq!(blank_label_guarded_preproc(src), src);
}
#[test]
fn leaves_label_followed_by_real_statement_untouched() {
let src = "\
void f(void) {
goto out;
out:
return;
}
";
assert_eq!(blank_label_guarded_preproc(src), src);
}
#[test]
fn leaves_a_recoverable_marker_encoding_the_guard() {
let src = "\
void f(void) {
goto out;
out:
#ifdef CONFIG_ELOOP_SELECT
os_free(rfds);
#endif
return;
}
";
let fixed = blank_label_guarded_preproc(src);
assert!(
fixed.contains("/*Gd:CONFIG_ELOOP_SELECT*/"),
"missing open marker in: {fixed:?}"
);
assert!(
fixed.contains("/*E*/"),
"missing close marker in: {fixed:?}"
);
assert!(parses_clean(src));
assert_eq!(fixed.len(), src.len());
}
#[test]
fn marker_distinguishes_ifdef_from_ifndef() {
let src_ifdef = "\
void f(void) {
goto out;
out:
#ifdef X
os_free(rfds);
#endif
return;
}
";
let src_ifndef = src_ifdef.replace("#ifdef X", "#ifndef X");
assert!(blank_label_guarded_preproc(src_ifdef).contains("/*Gd:X*/"));
assert!(blank_label_guarded_preproc(&src_ifndef).contains("/*Gn:X*/"));
}
#[test]
fn no_marker_for_bare_if_expression() {
let src = "\
void f(void) {
goto out;
out:
#if defined(X) && Y > 2
os_free(rfds);
#endif
return;
}
";
let fixed = blank_label_guarded_preproc(src);
assert!(!fixed.contains("/*G"));
assert!(!fixed.contains("/*E*/"));
assert!(parses_clean(src));
}
}