use crate::analyze::control_header_preproc_guard::{
blank_line, ends_with_control_header, is_branch_directive, is_directive, is_directive_start,
is_endif, strip_comments,
};
use crate::analyze::preproc_dangling_else::is_bare_else_line;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum ArmEnd {
ControlHeader,
BareElse,
Complete,
}
fn arm_body(lines: &[&str], lo: usize, hi: usize) -> Vec<usize> {
(lo..hi)
.filter(|&k| !lines[k].trim().is_empty() && !is_directive(lines[k]))
.collect()
}
fn joined(lines: &[&str], body: &[usize]) -> String {
body.iter()
.map(|&k| lines[k])
.collect::<Vec<_>>()
.join("\n")
}
fn classify(lines: &[&str], body: &[usize]) -> ArmEnd {
let Some(&last) = body.last() else {
return ArmEnd::Complete;
};
if ends_with_control_header(&joined(lines, body)) {
return ArmEnd::ControlHeader;
}
if is_bare_else_line(lines[last]) {
return ArmEnd::BareElse;
}
ArmEnd::Complete
}
fn fragment_lines(lines: &[&str], body: &[usize], end: ArmEnd) -> Vec<usize> {
match end {
ArmEnd::Complete => Vec::new(),
ArmEnd::BareElse => body.last().copied().into_iter().collect(),
ArmEnd::ControlHeader => {
for start in (0..body.len()).rev() {
if ends_with_control_header(&joined(lines, &body[start..])) {
return body[start..].to_vec();
}
}
body.to_vec()
}
}
}
fn shared_brace_follows(lines: &[&str], from: usize) -> bool {
lines[from..]
.iter()
.find(|l| !l.trim().is_empty())
.is_some_and(|l| strip_comments(l).trim() == "{")
}
pub fn blank_split_chain_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() {
if !is_directive_start(lines[i]) {
i += 1;
continue;
}
let mut depth = 1i32;
let mut end_idx = None;
let mut branches: Vec<usize> = Vec::new();
let mut j = i + 1;
while j < lines.len() {
if is_directive_start(lines[j]) {
depth += 1;
} else if is_endif(lines[j]) {
depth -= 1;
if depth == 0 {
end_idx = Some(j);
break;
}
} else if depth == 1 && is_branch_directive(lines[j]) {
branches.push(j);
}
j += 1;
}
let Some(end_idx) = end_idx else {
i += 1;
continue;
};
if branches.is_empty() || !shared_brace_follows(&lines, end_idx + 1) {
i += 1;
continue;
}
let mut edges = Vec::with_capacity(branches.len() + 2);
edges.push(i);
edges.extend(branches.iter().copied());
edges.push(end_idx);
let bodies: Vec<Vec<usize>> = edges
.windows(2)
.map(|e| arm_body(&lines, e[0] + 1, e[1]))
.collect();
let ends: Vec<ArmEnd> = bodies.iter().map(|b| classify(&lines, b)).collect();
let keep = ends.iter().rposition(|&e| e != ArmEnd::Complete);
let Some(keep) = keep else {
i += 1;
continue;
};
let mut victims: Vec<usize> = Vec::new();
for (a, body) in bodies.iter().enumerate() {
if a < keep {
victims.extend(fragment_lines(&lines, body, ends[a]));
} else if a > keep {
victims.extend(body.iter().copied());
}
}
for k in victims
.into_iter()
.chain(std::iter::once(i))
.chain(branches.iter().copied())
.chain(std::iter::once(end_idx))
{
blank_line(&mut out, line_starts[k], lines[k].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_split_chain_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()
}
fn unchanged(src: &str) -> bool {
blank_split_chain_preproc(src) == src
}
#[test]
fn fixes_two_arms_sharing_one_brace() {
let src = "\
int f(int x);
int g(int monitor, int count)
{
#if defined(USING_VERSION_SDL3)
if (f(monitor) != 0)
#else
if ((monitor >= 0) && (monitor < count))
#endif
{
return 1;
}
return 0;
}
";
assert!(parses_clean(src));
}
#[test]
fn keeps_the_last_incomplete_arm_and_only_the_others_fragments() {
let src = "\
int f(int x);
int g(int monitor, int count)
{
#if defined(A)
f(1);
if (f(monitor) != 0)
#else
if ((monitor >= 0) && (monitor < count))
#endif
{
return 1;
}
return 0;
}
";
let fixed = blank_split_chain_preproc(src);
assert!(fixed.contains(" f(1);"));
assert!(!fixed.contains("if (f(monitor) != 0)"));
assert!(fixed.contains("if ((monitor >= 0) && (monitor < count))"));
assert!(parses_clean(src));
}
#[test]
fn fixes_mixed_else_and_header_arms() {
let src = "\
int f(int x);
int g(void)
{
int h = 0, res = 0;
#ifdef HAVE_R_5
h = f(1);
if (h) {
;
}
else
#elif defined(HAVE_R_6)
h = f(2);
if (!h)
#elif defined(HAVE_R_3)
res = f(3);
if (!res) {
h = 1;
}
else
#endif
{
h = 0;
}
return h;
}
";
let fixed = blank_split_chain_preproc(src);
assert!(fixed.contains("h = f(1);"));
assert!(fixed.contains("h = f(2);"));
assert!(fixed.contains("res = f(3);"));
assert!(parses_clean(src));
}
#[test]
fn blanks_a_complete_arm_that_follows_the_kept_one() {
let src = "\
int f(int x);
void g(int argc)
{
#ifdef SQLITE_ENABLE_STAT4
int eCall = f(argc);
if (eCall == 1)
#else
f(argc);
#endif
{
f(0);
}
}
";
let fixed = blank_split_chain_preproc(src);
assert!(fixed.contains("if (eCall == 1)"));
assert!(!fixed.contains("\n f(argc);\n"));
assert!(parses_clean(src));
}
#[test]
fn fixes_a_condition_wrapped_across_lines() {
let src = "\
int f(int x);
int g(int a)
{
#ifdef A
if (f(a)
&& f(a + 1))
#else
if (f(a))
#endif
{
return 1;
}
return 0;
}
";
let fixed = blank_split_chain_preproc(src);
assert!(!fixed.contains("&& f(a + 1)"));
assert!(parses_clean(src));
}
#[test]
fn leaves_a_chain_with_no_shared_brace_alone() {
let src = "\
int f(int x);
int g(int a)
{
#ifdef A
return f(a);
#else
return f(a + 1);
#endif
}
";
assert!(unchanged(src));
}
#[test]
fn leaves_a_chain_whose_arms_end_complete_alone() {
let src = "\
int f(int x);
int g(int a)
{
#ifdef A
f(a);
#else
f(a + 1);
#endif
{
return 0;
}
}
";
assert!(unchanged(src));
}
#[test]
fn leaves_a_single_arm_chain_to_the_pass_that_owns_it() {
let src = "\
int f(int x);
int g(int a)
{
#ifdef A
if (f(a))
#endif
{
return 1;
}
return 0;
}
";
assert!(unchanged(src));
}
#[test]
fn handles_a_nested_chain_independently() {
let src = "\
int f(int x);
int g(int a)
{
#ifdef A
if (f(a) != 0)
#else
if (a >= 0)
#endif
{
#ifdef A
if (f(a + 1))
#else
if (f(a + 2) == 0)
#endif
{
return 1;
}
}
return 0;
}
";
assert!(parses_clean(src));
}
#[test]
fn is_length_preserving() {
let src = "\
int f(int x);
int g(int a)
{
#ifdef A
if (f(a))
#else
if (a)
#endif
{ return 1; }
return 0;
}
";
assert_eq!(blank_split_chain_preproc(src).len(), src.len());
}
}