use crate::graph::cfg_edges_extract::extract_cfg_condition;
use crate::graph::schema::CfgBlock;
use tree_sitter::Node;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockKind {
Entry,
If,
Else,
Loop,
While,
For,
MatchArm,
MatchMerge,
Return,
Break,
Continue,
Block,
}
impl BlockKind {
pub fn as_str(&self) -> &'static str {
match self {
BlockKind::Entry => "entry",
BlockKind::If => "if",
BlockKind::Else => "else",
BlockKind::Loop => "loop",
BlockKind::While => "while",
BlockKind::For => "for",
BlockKind::MatchArm => "match_arm",
BlockKind::MatchMerge => "match_merge",
BlockKind::Return => "return",
BlockKind::Break => "break",
BlockKind::Continue => "continue",
BlockKind::Block => "block",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminatorKind {
Fallthrough,
Conditional,
Goto,
Return,
Break,
Continue,
Call,
Panic,
}
impl TerminatorKind {
pub fn as_str(&self) -> &'static str {
match self {
TerminatorKind::Fallthrough => "fallthrough",
TerminatorKind::Conditional => "conditional",
TerminatorKind::Goto => "goto",
TerminatorKind::Return => "return",
TerminatorKind::Break => "break",
TerminatorKind::Continue => "continue",
TerminatorKind::Call => "call",
TerminatorKind::Panic => "panic",
}
}
}
fn find_function_body<'a>(func_node: &Node<'a>) -> Option<Node<'a>> {
let mut cursor = func_node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "block" {
return Some(child);
}
if !cursor.goto_next_sibling() {
break;
}
}
}
None
}
pub struct CfgExtractor<'a> {
_source: &'a [u8],
next_block_id: usize,
blocks: Vec<CfgBlock>,
}
impl<'a> CfgExtractor<'a> {
pub fn new(source: &'a [u8]) -> Self {
Self {
_source: source,
next_block_id: 0,
blocks: Vec::new(),
}
}
pub fn extract_cfg_from_function(
&mut self,
func_node: &Node,
function_id: i64,
) -> Vec<CfgBlock> {
self.blocks.clear();
self.next_block_id = 0;
let cfg_condition = if let Ok(source_str) = std::str::from_utf8(self._source) {
extract_cfg_condition(func_node, source_str)
} else {
None
};
if let Some(body_node) = find_function_body(func_node) {
let _entry_id = self.next_block_id;
self.next_block_id += 1;
self.visit_block(&body_node, function_id, BlockKind::Entry);
}
let mut blocks = std::mem::take(&mut self.blocks);
if let Some(ref cond) = cfg_condition {
for block in &mut blocks {
block.cfg_condition = Some(cond.clone());
}
}
blocks
}
fn visit_block(&mut self, node: &Node, function_id: i64, kind: BlockKind) {
let byte_start = node.start_byte() as u64;
let byte_end = node.end_byte() as u64;
let start_line = node.start_position().row as u64 + 1;
let start_col = node.start_position().column as u64;
let end_line = node.end_position().row as u64 + 1;
let end_col = node.end_position().column as u64;
let terminator = self.detect_block_terminator(node);
let block = CfgBlock {
function_id,
kind: kind.as_str().to_string(),
terminator: terminator.as_str().to_string(),
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
cfg_hash: None,
statements: None,
cfg_condition: None,
};
self.blocks.push(block);
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
self.visit_control_flow(&child, function_id);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn visit_control_flow(&mut self, node: &Node, function_id: i64) {
match node.kind() {
"if_expression" => self.visit_if(node, function_id),
"loop_expression" => self.visit_loop(node, function_id, BlockKind::Loop),
"while_expression" => self.visit_loop(node, function_id, BlockKind::While),
"for_expression" => self.visit_loop(node, function_id, BlockKind::For),
"match_expression" => self.visit_match(node, function_id),
"return_expression" => self.visit_return(node, function_id),
"break_expression" => self.visit_break(node, function_id),
"continue_expression" => self.visit_continue(node, function_id),
_ => {
if node.kind() == "block" || node.kind() == "expression_statement" {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
self.visit_control_flow(&child, function_id);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
}
}
}
fn visit_if(&mut self, node: &Node, function_id: i64) {
let mut cursor = node.walk();
let mut child_count = 0;
if cursor.goto_first_child() {
loop {
let child = cursor.node();
match child_count {
0 => {
}
1 => {
}
2 => {
if child.kind() == "block" {
self.visit_block(&child, function_id, BlockKind::If);
} else if child.kind() == "if_expression" {
self.visit_if(&child, function_id);
}
}
3 if child.kind() == "else_clause" => {
let mut else_cursor = child.walk();
if else_cursor.goto_first_child() {
loop {
let else_child = else_cursor.node();
if else_child.kind() == "block" {
self.visit_block(&else_child, function_id, BlockKind::Else);
} else if else_child.kind() == "if_expression" {
self.visit_if(&else_child, function_id);
}
if !else_cursor.goto_next_sibling() {
break;
}
}
}
}
_ => {}
}
child_count += 1;
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn visit_loop(&mut self, node: &Node, function_id: i64, kind: BlockKind) {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "block" {
self.visit_block(&child, function_id, kind.clone());
}
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn visit_match(&mut self, node: &Node, function_id: i64) {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "match_block" {
let mut block_cursor = child.walk();
if block_cursor.goto_first_child() {
loop {
let block_child = block_cursor.node();
if block_child.kind() == "match_arm" {
self.visit_match_arm(&block_child, function_id);
}
if !block_cursor.goto_next_sibling() {
break;
}
}
}
}
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn visit_match_arm(&mut self, node: &Node, function_id: i64) {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "block" {
self.visit_block(&child, function_id, BlockKind::MatchArm);
}
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn visit_return(&mut self, node: &Node, function_id: i64) {
let byte_start = node.start_byte() as u64;
let byte_end = node.end_byte() as u64;
let start_line = node.start_position().row as u64 + 1;
let start_col = node.start_position().column as u64;
let end_line = node.end_position().row as u64 + 1;
let end_col = node.end_position().column as u64;
let block = CfgBlock {
function_id,
kind: BlockKind::Return.as_str().to_string(),
terminator: TerminatorKind::Return.as_str().to_string(),
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
cfg_hash: None,
statements: None,
cfg_condition: None,
};
self.blocks.push(block);
}
fn visit_break(&mut self, node: &Node, function_id: i64) {
let byte_start = node.start_byte() as u64;
let byte_end = node.end_byte() as u64;
let start_line = node.start_position().row as u64 + 1;
let start_col = node.start_position().column as u64;
let end_line = node.end_position().row as u64 + 1;
let end_col = node.end_position().column as u64;
let block = CfgBlock {
function_id,
kind: BlockKind::Break.as_str().to_string(),
terminator: TerminatorKind::Break.as_str().to_string(),
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
cfg_hash: None,
statements: None,
cfg_condition: None,
};
self.blocks.push(block);
}
fn visit_continue(&mut self, node: &Node, function_id: i64) {
let byte_start = node.start_byte() as u64;
let byte_end = node.end_byte() as u64;
let start_line = node.start_position().row as u64 + 1;
let start_col = node.start_position().column as u64;
let end_line = node.end_position().row as u64 + 1;
let end_col = node.end_position().column as u64;
let block = CfgBlock {
function_id,
kind: BlockKind::Continue.as_str().to_string(),
terminator: TerminatorKind::Continue.as_str().to_string(),
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
cfg_hash: None,
statements: None,
cfg_condition: None,
};
self.blocks.push(block);
}
fn detect_block_terminator(&self, node: &Node) -> TerminatorKind {
let mut cursor = node.walk();
let mut last_statement = None;
if cursor.goto_first_child() {
loop {
let current = cursor.node();
last_statement = Some(current);
if !cursor.goto_next_sibling() {
break;
}
}
}
if let Some(last) = last_statement {
match last.kind() {
"return_expression" => TerminatorKind::Return,
"break_expression" => TerminatorKind::Break,
"continue_expression" => TerminatorKind::Continue,
"if_expression" => TerminatorKind::Conditional,
"match_expression" => TerminatorKind::Conditional,
"loop_expression" | "while_expression" | "for_expression" => {
TerminatorKind::Conditional
}
"call_expression" => TerminatorKind::Call,
_ => TerminatorKind::Fallthrough,
}
} else {
TerminatorKind::Fallthrough
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
fn parse_rust(source: &[u8]) -> tree_sitter::Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_rust::LANGUAGE.into())
.unwrap();
parser.parse(source, None).unwrap()
}
fn find_first_function(tree: &tree_sitter::Tree) -> Option<Node<'_>> {
let root = tree.root_node();
let mut cursor = root.walk();
let first_function = root
.children(&mut cursor)
.find(|child| child.kind() == "function_item");
first_function
}
#[test]
fn test_extract_simple_function() {
let source = b"fn main() { let x = 1; }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
assert_eq!(blocks[0].kind, "entry");
}
#[test]
fn test_extract_if_function() {
let source = b"fn test() { if x { y } else { z } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let if_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "if").collect();
assert!(!if_blocks.is_empty());
}
#[test]
fn test_extract_loop_function() {
let source = b"fn test() { loop { break; } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let loop_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "loop").collect();
assert!(!loop_blocks.is_empty());
}
#[test]
fn test_extract_return() {
let source = b"fn test() { return 42; }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let return_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "return").collect();
assert!(!return_blocks.is_empty());
assert_eq!(return_blocks[0].terminator, "return");
}
#[test]
fn test_extract_match() {
let source = b"fn test(x: i32) { match x { 1 => {}, _ => {} } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let match_arms: Vec<_> = blocks.iter().filter(|b| b.kind == "match_arm").collect();
assert!(!match_arms.is_empty());
}
#[test]
fn test_block_kind_display() {
assert_eq!(BlockKind::Entry.as_str(), "entry");
assert_eq!(BlockKind::If.as_str(), "if");
assert_eq!(BlockKind::Loop.as_str(), "loop");
assert_eq!(BlockKind::Return.as_str(), "return");
}
#[test]
fn test_terminator_kind_display() {
assert_eq!(TerminatorKind::Return.as_str(), "return");
assert_eq!(TerminatorKind::Break.as_str(), "break");
assert_eq!(TerminatorKind::Conditional.as_str(), "conditional");
}
#[test]
fn test_extract_while_loop() {
let source = b"fn test() { while x { y } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let while_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "while").collect();
assert!(!while_blocks.is_empty());
}
#[test]
fn test_extract_for_loop() {
let source = b"fn test() { for x in y { z } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let for_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "for").collect();
assert!(!for_blocks.is_empty());
}
#[test]
fn test_extract_break() {
let source = b"fn test() { loop { break; } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let break_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "break").collect();
assert!(!break_blocks.is_empty());
assert_eq!(break_blocks[0].terminator, "break");
}
#[test]
fn test_extract_continue() {
let source = b"fn test() { loop { continue; } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let continue_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "continue").collect();
assert!(!continue_blocks.is_empty());
assert_eq!(continue_blocks[0].terminator, "continue");
}
#[test]
fn test_extract_nested_if() {
let source = b"fn test() { if x { if y { z } } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let if_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "if").collect();
assert!(if_blocks.len() >= 2);
}
#[test]
fn test_extract_else_if() {
let source = b"fn test() { if x { y } else if z { w } }";
let tree = parse_rust(source);
let func = find_first_function(&tree).unwrap();
let mut extractor = CfgExtractor::new(source);
let blocks = extractor.extract_cfg_from_function(&func, 1);
assert!(!blocks.is_empty());
let if_blocks: Vec<_> = blocks.iter().filter(|b| b.kind == "if").collect();
assert!(!if_blocks.is_empty());
}
}