#![allow(clippy::disallowed_types)]
use std::collections::BTreeMap;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use codehelion_helper::ir::{Anchor, BasicBlock, ControlFlowGraph, Edge, EdgeKind};
use crate::database::ValidatedArguments;
static CLANG_AVAILABLE: OnceLock<bool> = OnceLock::new();
static CLANGXX_AVAILABLE: OnceLock<bool> = OnceLock::new();
#[derive(Debug, Clone)]
pub(crate) struct FunctionAnchor {
pub(crate) name: String,
pub(crate) anchor: Anchor,
}
pub(crate) fn available() -> bool {
compiler_available("clang") || compiler_available("clang++")
}
fn compiler_available(name: &str) -> bool {
let cache = match name {
"clang" => &CLANG_AVAILABLE,
"clang++" => &CLANGXX_AVAILABLE,
_ => return false,
};
cached_availability(cache, || probe_compiler(name))
}
fn cached_availability(cache: &OnceLock<bool>, probe: impl FnOnce() -> bool) -> bool {
*cache.get_or_init(probe)
}
fn probe_compiler(name: &str) -> bool {
Command::new(name)
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
pub(crate) fn produce(
file: &Path,
arguments: &ValidatedArguments,
functions: &[FunctionAnchor],
) -> Option<ControlFlowGraph> {
let compiler = compiler_for(file)?;
let output = Command::new(compiler)
.arg("--no-default-config")
.args(arguments.as_slice())
.arg("-fsyntax-only")
.args([
"-Xclang",
"-analyze",
"-Xclang",
"-analyzer-checker=debug.DumpCFG",
])
.arg(file)
.stdin(Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let output = String::from_utf8(output.stderr).ok()?;
from_dump(&output, functions)
}
fn compiler_for(path: &Path) -> Option<&'static str> {
let extension = path.extension()?.to_str()?.to_ascii_lowercase();
match extension.as_str() {
"c" => compiler_available("clang").then_some("clang"),
"cc" | "cp" | "cpp" | "cxx" | "c++" => compiler_available("clang++").then_some("clang++"),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DumpBlock {
number: u32,
statements: u32,
successors: Vec<u32>,
conditional: bool,
entry: bool,
exit: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DumpFunction {
heading: String,
blocks: Vec<DumpBlock>,
}
fn from_dump(text: &str, functions: &[FunctionAnchor]) -> Option<ControlFlowGraph> {
let dumped = dump_functions(text);
let mut graph = ControlFlowGraph::default();
for function in functions {
let matching: Vec<_> = dumped
.iter()
.filter(|candidate| heading_names(candidate, &function.name))
.collect();
if matching.len() != 1
|| functions
.iter()
.filter(|candidate| candidate.name == function.name)
.count()
!= 1
{
continue;
}
append_function(&mut graph, matching[0], &function.anchor);
}
(!graph.blocks.is_empty()).then_some(graph)
}
fn heading_names(function: &DumpFunction, name: &str) -> bool {
function
.heading
.split_once('(')
.and_then(|(before_parameters, _)| before_parameters.split_whitespace().last())
.and_then(|qualified| qualified.rsplit("::").next())
== Some(name)
}
fn append_function(graph: &mut ControlFlowGraph, function: &DumpFunction, anchor: &Anchor) {
let ordinary: Vec<_> = function
.blocks
.iter()
.filter(|block| !block.entry && !block.exit)
.collect();
let start = u32::try_from(graph.blocks.len()).unwrap_or(u32::MAX);
let mut indices = BTreeMap::new();
for (offset, block) in ordinary.iter().enumerate() {
let Ok(offset) = u32::try_from(offset) else {
return;
};
let Some(index) = start.checked_add(offset) else {
return;
};
indices.insert(block.number, index);
}
if indices.len() != ordinary.len() {
return;
}
graph.blocks.extend(ordinary.iter().map(|block| BasicBlock {
anchor: anchor.clone(),
length: block.statements,
}));
for block in ordinary {
let Some(&from) = indices.get(&block.number) else {
continue;
};
for (position, successor) in block.successors.iter().enumerate() {
let kind = edge_kind(block, position);
if let Some(&to) = indices.get(successor) {
graph.edges.push(Edge { from, to, kind });
} else if *successor == 0 {
graph.edges.push(Edge {
from,
to: from,
kind: if block.conditional {
kind
} else {
EdgeKind::Return
},
});
}
}
}
}
fn edge_kind(block: &DumpBlock, position: usize) -> EdgeKind {
if block.conditional && block.successors.len() == 2 {
return if position == 0 {
EdgeKind::Taken
} else {
EdgeKind::NotTaken
};
}
EdgeKind::Flow
}
fn dump_functions(text: &str) -> Vec<DumpFunction> {
let mut functions = Vec::new();
let mut heading: Option<String> = None;
let mut blocks = Vec::new();
let mut current: Option<DumpBlock> = None;
for line in text.lines() {
let trimmed = line.trim();
if !line.starts_with(char::is_whitespace) && !trimmed.is_empty() {
finish_block(&mut blocks, &mut current);
if !blocks.is_empty() {
if let Some(heading) = heading.take() {
functions.push(DumpFunction { heading, blocks });
}
blocks = Vec::new();
}
heading = Some(trimmed.to_owned());
continue;
}
if let Some((number, entry, exit)) = block_number(trimmed) {
finish_block(&mut blocks, &mut current);
current = Some(DumpBlock {
number,
statements: 0,
successors: Vec::new(),
conditional: false,
entry,
exit,
});
} else if trimmed.starts_with("T:") {
if let Some(block) = &mut current {
block.conditional = true;
}
} else if let Some(rest) = trimmed.strip_prefix("Succs (")
&& let Some((_, numbers)) = rest.split_once(": ")
&& let Some(block) = &mut current
{
block.successors = numbers
.split_whitespace()
.filter_map(|value| value.strip_prefix('B'))
.filter_map(|value| value.parse().ok())
.collect();
} else if trimmed
.chars()
.next()
.is_some_and(|character| character.is_ascii_digit())
&& let Some(block) = &mut current
{
block.statements = block.statements.saturating_add(1);
}
}
finish_block(&mut blocks, &mut current);
if !blocks.is_empty()
&& let Some(heading) = heading
{
functions.push(DumpFunction { heading, blocks });
}
functions
}
fn finish_block(blocks: &mut Vec<DumpBlock>, current: &mut Option<DumpBlock>) {
if let Some(block) = current.take() {
blocks.push(block);
}
}
fn block_number(line: &str) -> Option<(u32, bool, bool)> {
let rest = line.strip_prefix("[B")?;
let number = rest.split([' ', ']']).next()?.parse().ok()?;
Some((number, rest.contains("(ENTRY)"), rest.contains("(EXIT)")))
}
#[cfg(test)]
mod tests {
use super::*;
use codehelion_helper::ir::{Anchor, SourceRange};
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn compiler_availability_is_probed_once_per_executable() {
let cache = OnceLock::new();
let probes = AtomicUsize::new(0);
let probe = || {
probes.fetch_add(1, Ordering::Relaxed);
true
};
assert!(cached_availability(&cache, probe));
assert!(cached_availability(&cache, || false));
assert_eq!(probes.load(Ordering::Relaxed), 1);
}
fn anchor() -> Anchor {
Anchor::written_here(SourceRange {
file: "source.cpp".to_string(),
start_byte: 0,
end_byte: 20,
start_line: 1,
})
}
#[test]
#[allow(clippy::panic)]
fn maps_a_conditional_dump_without_entry_or_exit_blocks() {
let text = concat!(
"int choose(bool value)\n",
" [B3 (ENTRY)]\n",
" Succs (1): B2\n",
" [B2]\n",
" 1: value\n",
" T: if (value)\n",
" Succs (2): B1 B0\n",
" [B1]\n",
" 1: return 1;\n",
" Succs (1): B0\n",
" [B0 (EXIT)]\n",
" Preds (2): B2 B1\n",
);
let graph = from_dump(
text,
&[FunctionAnchor {
name: "choose".to_string(),
anchor: anchor(),
}],
)
.unwrap_or_else(|| panic!("one unambiguous CFG: {:?}", dump_functions(text)));
assert_eq!(graph.blocks.len(), 2);
assert_eq!(graph.blocks[0].length, 1);
assert_eq!(
graph.edges,
vec![
Edge {
from: 0,
to: 1,
kind: EdgeKind::Taken,
},
Edge {
from: 0,
to: 0,
kind: EdgeKind::NotTaken,
},
Edge {
from: 1,
to: 1,
kind: EdgeKind::Return,
},
]
);
}
#[test]
fn refuses_to_anchor_overloaded_functions_by_name() {
assert!(
from_dump(
"int value()\n [B1]\n Succs (1): B0\n [B0 (EXIT)]\n",
&[
FunctionAnchor {
name: "value".to_string(),
anchor: anchor(),
},
FunctionAnchor {
name: "value".to_string(),
anchor: anchor(),
},
],
)
.is_none()
);
}
#[test]
fn heading_name_requires_the_final_identifier_to_match() {
let function = DumpFunction {
heading: "int namespace::total_sum(int value)".to_string(),
blocks: Vec::new(),
};
assert!(!heading_names(&function, "sum"));
assert!(heading_names(&function, "total_sum"));
}
}