use lang_parsing_substrate::query;
use std::ops::Range;
use tree_sitter::Node;
type ArmId = (u32, u32);
#[derive(Clone, Debug, Default)]
pub struct PreprocArms {
boundaries: Vec<usize>,
paths: Vec<Vec<ArmId>>,
}
impl PreprocArms {
pub fn collect(root: &Node) -> Self {
let mut arms: Vec<(Range<usize>, ArmId)> = Vec::new();
let mut chain_id: u32 = 0;
for head in query::find_descendants_of_kinds(*root, &["preproc_if", "preproc_ifdef"]) {
let mut spans: Vec<Range<usize>> = Vec::new();
let mut start = head.start_byte();
let mut current = head;
loop {
match current.child_by_field_name("alternative") {
Some(alternative) => {
spans.push(start..alternative.start_byte());
start = alternative.start_byte();
current = alternative;
}
None => {
spans.push(start..current.end_byte());
break;
}
}
}
if spans.len() < 2 {
continue;
}
for (index, span) in spans.into_iter().enumerate() {
arms.push((span, (chain_id, index as u32)));
}
chain_id += 1;
}
if arms.is_empty() {
return Self::default();
}
let mut boundaries: Vec<usize> = arms
.iter()
.flat_map(|(span, _)| [span.start, span.end])
.collect();
boundaries.sort_unstable();
boundaries.dedup();
let paths = boundaries
.windows(2)
.map(|edges| {
let probe = edges[0];
let mut enclosing: Vec<ArmId> = arms
.iter()
.filter(|(span, _)| span.contains(&probe))
.map(|(_, id)| *id)
.collect();
enclosing.sort_unstable();
enclosing
})
.collect();
Self { boundaries, paths }
}
pub fn exclusive(&self, a: usize, b: usize) -> bool {
if self.paths.is_empty() {
return false;
}
let (Some(first), Some(second)) = (self.path_at(a), self.path_at(b)) else {
return false;
};
let (mut i, mut j) = (0, 0);
while i < first.len() && j < second.len() {
let ((left_chain, left_arm), (right_chain, right_arm)) = (first[i], second[j]);
match left_chain.cmp(&right_chain) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
if left_arm != right_arm {
return true;
}
i += 1;
j += 1;
}
}
}
false
}
fn path_at(&self, pos: usize) -> Option<&[ArmId]> {
let region = self.boundaries.partition_point(|edge| *edge <= pos);
let path = self.paths.get(region.checked_sub(1)?)?;
if path.is_empty() {
None
} else {
Some(path)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::c_language;
fn arms_of(source: &str) -> PreprocArms {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&c_language()).unwrap();
let tree = parser.parse(source, None).unwrap();
PreprocArms::collect(&tree.root_node())
}
fn at(source: &str, needle: &str) -> usize {
source.find(needle).expect("needle present in fixture")
}
#[test]
fn two_arms_of_one_ifdef_are_exclusive() {
let source = "void f(void)\n{\n#ifdef A\n\tint x = 1;\n#else\n\tint y = 2;\n#endif\n}\n";
let arms = arms_of(source);
assert!(arms.exclusive(at(source, "int x"), at(source, "int y")));
}
#[test]
fn one_arm_is_not_exclusive_with_itself() {
let source = "void f(void)\n{\n#ifdef A\n\tint x = 1;\n\tint z = x;\n#else\n\tint y = 2;\n#endif\n}\n";
let arms = arms_of(source);
assert!(!arms.exclusive(at(source, "int x"), at(source, "int z")));
}
#[test]
fn code_outside_the_chain_coexists_with_both_arms() {
let source = "void f(void)\n{\n\tint w = 0;\n#ifdef A\n\tint x = 1;\n#else\n\tint y = 2;\n#endif\n}\n";
let arms = arms_of(source);
assert!(!arms.exclusive(at(source, "int w"), at(source, "int x")));
assert!(!arms.exclusive(at(source, "int w"), at(source, "int y")));
}
#[test]
fn an_elif_arm_is_exclusive_with_both_of_its_neighbours() {
let source = "void f(void)\n{\n#if A\n\tint x = 1;\n#elif B\n\tint y = 2;\n#else\n\tint z = 3;\n#endif\n}\n";
let arms = arms_of(source);
assert!(arms.exclusive(at(source, "int x"), at(source, "int y")));
assert!(arms.exclusive(at(source, "int y"), at(source, "int z")));
assert!(arms.exclusive(at(source, "int x"), at(source, "int z")));
}
#[test]
fn a_chain_with_no_else_never_separates_anything() {
let source = "void f(void)\n{\n#ifdef A\n\tint x = 1;\n#endif\n\tint y = 2;\n}\n";
let arms = arms_of(source);
assert!(!arms.exclusive(at(source, "int x"), at(source, "int y")));
}
#[test]
fn code_after_the_last_endif_coexists_with_both_arms() {
let source = "void f(void)\n{\n#ifdef A\n\tint x = 1;\n#else\n\tint y = 2;\n#endif\n\tint w = 0;\n}\n";
let arms = arms_of(source);
assert!(!arms.exclusive(at(source, "int w"), at(source, "int x")));
assert!(!arms.exclusive(at(source, "int w"), at(source, "int y")));
}
#[test]
fn two_sequential_chains_do_not_separate_their_first_arms() {
let source = concat!(
"void f(void)\n{\n#ifdef A\n\tint x = 1;\n#else\n\tint p = 0;\n#endif\n",
"#ifdef B\n\tint y = 2;\n#else\n\tint q = 0;\n#endif\n}\n"
);
let arms = arms_of(source);
assert!(!arms.exclusive(at(source, "int x"), at(source, "int y")));
assert!(arms.exclusive(at(source, "int x"), at(source, "int p")));
assert!(arms.exclusive(at(source, "int y"), at(source, "int q")));
}
#[test]
fn a_nested_chain_splits_within_an_arm() {
let source = concat!(
"void f(void)\n{\n#ifdef A\n",
"#ifdef B\n\tint x = 1;\n#else\n\tint y = 2;\n#endif\n",
"#else\n\tint z = 3;\n#endif\n}\n"
);
let arms = arms_of(source);
assert!(arms.exclusive(at(source, "int x"), at(source, "int y")));
assert!(arms.exclusive(at(source, "int x"), at(source, "int z")));
assert!(arms.exclusive(at(source, "int y"), at(source, "int z")));
}
}