const MAX_STATEMENT_LOOKBACK: usize = 64;
fn directive_keyword(line: &str) -> Option<&str> {
let rest = line.trim_start().strip_prefix('#')?;
Some(
rest.trim_start()
.split(|c: char| !c.is_alphanumeric())
.next()
.unwrap_or(""),
)
}
fn is_directive_start(line: &str) -> bool {
matches!(directive_keyword(line), Some("if" | "ifdef" | "ifndef"))
}
fn is_endif(line: &str) -> bool {
directive_keyword(line) == Some("endif")
}
fn is_branch_directive(line: &str) -> bool {
matches!(directive_keyword(line), Some("else" | "elif"))
}
fn is_directive(line: &str) -> bool {
line.trim_start().starts_with('#')
}
struct CodeLines<'a> {
raw: Vec<&'a str>,
code: Vec<String>,
}
impl<'a> CodeLines<'a> {
fn new(source: &'a str) -> Self {
let raw: Vec<&str> = source.lines().collect();
let mut code = Vec::with_capacity(raw.len());
let mut in_block_comment = false;
let mut in_directive = false;
for line in &raw {
let (stripped, still_open) = strip_comments_and_literals(line, in_block_comment);
in_block_comment = still_open;
let is_directive_line = in_directive || is_directive(line);
in_directive = is_directive_line && line.trim_end().ends_with('\\');
code.push(if is_directive_line {
" ".repeat(stripped.len())
} else {
stripped
});
}
Self { raw, code }
}
fn len(&self) -> usize {
self.raw.len()
}
fn paren_balance(&self, i: usize) -> i32 {
let line = &self.code[i];
line.matches('(').count() as i32 - line.matches(')').count() as i32
}
fn ends_statement(&self, i: usize) -> bool {
matches!(
self.code[i].trim_end().chars().next_back(),
Some(';' | '{' | '}')
)
}
}
fn strip_comments_and_literals(line: &str, mut in_block_comment: bool) -> (String, bool) {
let bytes = line.as_bytes();
let mut out = vec![b' '; bytes.len()];
let mut i = 0usize;
while i < bytes.len() {
if in_block_comment {
if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
in_block_comment = false;
i += 2;
} else {
i += 1;
}
continue;
}
match bytes[i] {
b'/' if bytes.get(i + 1) == Some(&b'*') => {
in_block_comment = true;
i += 2;
}
b'/' if bytes.get(i + 1) == Some(&b'/') => break,
quote @ (b'"' | b'\'') => {
i += 1;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == quote {
i += 1;
break;
}
i += 1;
}
}
c => {
out[i] = c;
i += 1;
}
}
}
(
String::from_utf8(out).unwrap_or_else(|_| " ".repeat(bytes.len())),
in_block_comment,
)
}
fn inside_unclosed_paren(lines: &CodeLines, i: usize) -> bool {
let mut balance = 0i32;
let mut scanned = 0usize;
let mut k = i;
while k > 0 && scanned < MAX_STATEMENT_LOOKBACK {
k -= 1;
scanned += 1;
if lines.raw[k].trim().is_empty() {
break;
}
balance += lines.paren_balance(k);
if lines.ends_statement(k) {
break;
}
}
balance > 0
}
pub fn blank_paren_guarded_preproc(source: &str) -> String {
let lines = CodeLines::new(source);
let mut line_starts = Vec::with_capacity(lines.len());
let mut offset = 0usize;
for line in &lines.raw {
line_starts.push(offset);
offset += line.len() + 1; }
let mut out = source.as_bytes().to_vec();
for i in 0..lines.len() {
if !is_directive_start(lines.raw[i]) || !inside_unclosed_paren(&lines, i) {
continue;
}
let mut depth = 1i32;
let mut end_idx = None;
let mut has_branch = false;
for (j, raw) in lines.raw.iter().enumerate().skip(i + 1) {
if is_directive_start(raw) {
depth += 1;
} else if is_endif(raw) {
depth -= 1;
if depth == 0 {
end_idx = Some(j);
break;
}
} else if depth == 1 && is_branch_directive(raw) {
has_branch = true;
}
}
let Some(end_idx) = end_idx else {
continue; };
if has_branch {
continue;
}
let body = i + 1..end_idx;
let is_expression_fragment = body
.clone()
.all(|k| !lines.code[k].contains([';', '{', '}']));
let balanced = body.clone().map(|k| lines.paren_balance(k)).sum::<i32>() == 0;
if !is_expression_fragment || !balanced {
continue;
}
blank_line(&mut out, line_starts[i], lines.raw[i].len());
blank_line(&mut out, line_starts[end_idx], lines.raw[end_idx].len());
}
String::from_utf8(out).unwrap_or_else(|_| source.to_string())
}
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' ';
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::c_language;
fn fixed(src: &str) -> String {
let out = blank_paren_guarded_preproc(src);
assert_eq!(src.len(), out.len(), "byte length changed");
assert_eq!(
src.matches('\n').count(),
out.matches('\n').count(),
"line count changed"
);
out
}
fn parses_clean(src: &str) -> bool {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&c_language()).unwrap();
let tree = parser.parse(fixed(src), None).unwrap();
!tree.root_node().has_error()
}
#[test]
fn fixes_ifdef_splitting_an_if_condition() {
let src = "\
int f(int a, int b) {
if (a
#ifdef X
&& b
#endif
) {
return 1;
}
return 0;
}
";
assert!(parses_clean(src));
assert!(!fixed(src).contains("#ifdef X"));
}
#[test]
fn fixes_a_space_between_hash_and_keyword() {
let src = "\
int f(int a) {
if (
# ifndef ALWAYS_SHOW_RESOLVED_SYMLINKS
compat != 0 &&
# endif
g(a)) {
return 1;
}
return 0;
}
";
assert!(parses_clean(src));
}
#[test]
fn fixes_ifdef_in_a_parameter_list() {
let src = "\
static void g(
#ifdef X
int a,
#endif
int b)
{
}
";
assert!(parses_clean(src));
}
#[test]
fn fixes_ifdef_in_an_argument_list() {
let src = "\
void h(void) {
foo(1,
#ifdef X
2,
#endif
3);
}
";
assert!(parses_clean(src));
}
#[test]
fn leaves_a_statement_level_guard_alone() {
let src = "\
void f(void) {
int x = 0;
#ifdef X
x = 1;
#endif
(void) x;
}
";
assert_eq!(blank_paren_guarded_preproc(src), src);
}
#[test]
fn leaves_a_two_branch_block_alone() {
let src = "\
void f(void) {
foo(a &&
#ifdef DEBUGBUILD
getenv(\"X\")
#else
0
#endif
);
}
";
assert_eq!(blank_paren_guarded_preproc(src), src);
}
#[test]
fn leaves_an_unbalanced_body_alone() {
let src = "\
void f(void) {
foo(a,
#ifdef X
bar(b,
#endif
c));
}
";
assert_eq!(blank_paren_guarded_preproc(src), src);
}
#[test]
fn a_branch_with_unbalanced_parens_does_not_poison_later_guards() {
let src = "\
void f(int a) {
#ifdef X
g((a);
#else
g(a));
#endif
int x = 0;
#ifdef Y
x = 1;
#endif
(void) x;
}
";
assert_eq!(blank_paren_guarded_preproc(src), src);
}
#[test]
fn a_guard_inside_a_comment_or_string_is_not_a_directive() {
let src = "\
void f(void) {
const char *s = \"#ifdef X\";
/* #ifdef Y */
foo(s);
}
";
assert_eq!(blank_paren_guarded_preproc(src), src);
}
}