fn strip_trailing_line_comment(s: &str) -> &str {
match s.find("//") {
Some(idx) => s[..idx].trim_end(),
None => s,
}
}
fn starts_with_bare_else(line: &str) -> bool {
match line.strip_prefix("else") {
Some(rest) => rest
.chars()
.next()
.is_none_or(|c| !c.is_ascii_alphanumeric() && c != '_'),
None => false,
}
}
fn is_bare_else_line(line: &str) -> bool {
strip_trailing_line_comment(line.trim()) == "else"
}
fn ends_with_dangling_else_open_brace(line: &str) -> bool {
let t = strip_trailing_line_comment(line.trim());
let Some(before) = t.strip_suffix('{') else {
return false;
};
let before = before.trim_end();
let Some(before) = before.strip_suffix("else") else {
return false;
};
if before
.chars()
.next_back()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
{
return false;
}
before.trim_end().ends_with('}')
}
fn block_is_lone_closing_brace(lines: &[&str], start_idx: usize, end_idx: usize) -> bool {
let mut content = (start_idx..end_idx).filter(|&k| !lines[k].trim().is_empty());
let Some(only_line) = content.next() else {
return false;
};
if content.next().is_some() {
return false;
}
strip_trailing_line_comment(lines[only_line].trim()) == "}"
}
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 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' ';
}
}
}
pub fn blank_dangling_else_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 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 body = (i + 1)..end_idx;
let first_content = body.clone().find(|&k| !lines[k].trim().is_empty());
let last_content = body.clone().rev().find(|&k| !lines[k].trim().is_empty());
let leading_else =
first_content.is_some_and(|k| starts_with_bare_else(lines[k].trim_start()));
let trailing_else = last_content.is_some_and(|k| is_bare_else_line(lines[k]));
let trailing_dangling_open_brace =
last_content.is_some_and(|k| ends_with_dangling_else_open_brace(lines[k]));
let lone_closing_brace = block_is_lone_closing_brace(&lines, i + 1, end_idx);
if leading_else || trailing_else || trailing_dangling_open_brace || lone_closing_brace {
blank_line(&mut out, line_starts[i], lines[i].len());
blank_line(&mut out, line_starts[end_idx], lines[end_idx].len());
}
}
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_dangling_else_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_leading_dangling_else() {
let src = "\
int f(int fmt) {
int channels = 0;
if (false) { }
#if SUPPORT_FILEFORMAT_PNG
else if (fmt == 1) { channels = 1; }
#endif
#if SUPPORT_FILEFORMAT_QOI
else if (fmt == 2)
{
channels = 2;
}
#endif
else { channels = 3; }
return channels;
}
";
assert!(parses_clean(src));
}
#[test]
fn fixes_trailing_dangling_else() {
let src = "\
int f(int fmt) {
int x;
#if SUPPORT_FILEFORMAT_TTF
if (fmt == 1) x = 1;
else
#endif
#if SUPPORT_FILEFORMAT_FNT
if (fmt == 2) x = 2;
else
#endif
{
x = 3;
}
return x;
}
";
assert!(parses_clean(src));
}
#[test]
fn preserves_byte_length_and_line_count() {
let src = "\
int f(int fmt) {
int channels = 0;
if (false) { }
#if SUPPORT_FILEFORMAT_PNG
else if (fmt == 1) { channels = 1; }
#endif
else { channels = 3; }
return channels;
}
";
let fixed = blank_dangling_else_preproc(src);
assert_eq!(fixed.len(), src.len());
assert_eq!(fixed.matches('\n').count(), src.matches('\n').count());
let pos_orig = src.find("return channels;").unwrap();
let pos_fixed = fixed.find("return channels;").unwrap();
assert_eq!(pos_orig, pos_fixed);
}
#[test]
fn leaves_normal_if_else_if_chain_untouched() {
let src = "\
int f(int x) {
int y;
if (x == 1) y = 1;
else if (x == 2) y = 2;
else y = 3;
return y;
}
";
assert_eq!(blank_dangling_else_preproc(src), src);
}
#[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_dangling_else_preproc(src), src);
}
#[test]
fn fixes_dangling_else_nested_inside_an_outer_guard() {
let src = "\
#if SUPPORT_IMAGE_EXPORT
int f(int fmt) {
int channels = 0;
if (false) { }
#if SUPPORT_FILEFORMAT_PNG
else if (fmt == 1) { channels = 1; }
#endif
else { channels = 3; }
return channels;
}
#endif
";
assert!(parses_clean(src));
}
#[test]
fn skips_block_with_its_own_else_branch() {
let src = "\
int f(int x) {
int y = 0;
if (false) { }
#if COND
else if (x == 1) { y = 1; }
#else
else { y = 2; }
#endif
return y;
}
";
assert_eq!(blank_dangling_else_preproc(src), src);
}
#[test]
fn fixes_brace_else_open_brace_split_across_two_guards() {
let src = "\
int f(int version) {
int hlen;
#ifdef CONFIG_TLSV12
if (version == 2) {
hlen = 1;
} else {
#endif
hlen = 2;
#ifdef CONFIG_TLSV12
}
#endif
return hlen;
}
";
assert!(parses_clean(src));
}
#[test]
fn brace_else_open_brace_preserves_byte_length_and_line_count() {
let src = "\
int f(int version) {
int hlen;
#ifdef CONFIG_TLSV12
if (version == 2) {
hlen = 1;
} else {
#endif
hlen = 2;
#ifdef CONFIG_TLSV12
}
#endif
return hlen;
}
";
let fixed = blank_dangling_else_preproc(src);
assert_eq!(fixed.len(), src.len());
assert_eq!(fixed.matches('\n').count(), src.matches('\n').count());
let pos_orig = src.find("return hlen;").unwrap();
let pos_fixed = fixed.find("return hlen;").unwrap();
assert_eq!(pos_orig, pos_fixed);
}
#[test]
fn leaves_lone_brace_guard_with_matching_open_untouched() {
let src = "\
int f(void) {
if (1) {
#ifdef X
}
#endif
return 0;
}
";
assert!(parses_clean(src));
}
#[test]
fn leaves_else_word_prefix_identifier_untouched() {
assert!(!ends_with_dangling_else_open_brace("} elsewhere_flag {"));
}
}