use crate::util::{indent_level, mask_strings};
pub trait FunctionDetectionMarkers {
fn function_markers(&self) -> &[&str];
fn brace_scoped(&self) -> bool;
fn line_comments(&self) -> &[&str];
}
pub struct FunctionBody<'a> {
pub name: String,
pub start_line: usize,
pub code_lines: Vec<(usize, &'a str)>,
}
const CONTROL_KEYWORDS: &[&str] = &[
"if", "for", "while", "switch", "else", "do", "catch", "return", "case",
];
fn is_function_declaration(trimmed: &str, markers: &dyn FunctionDetectionMarkers) -> bool {
if markers
.function_markers()
.iter()
.any(|m| trimmed.contains(m))
{
return true;
}
is_c_family_function(trimmed)
}
fn is_c_family_function(trimmed: &str) -> bool {
if trimmed.starts_with('#') {
return false;
}
let paren_pos = match trimmed.find('(') {
Some(p) => p,
None => return false,
};
if !(trimmed.ends_with('{') || trimmed.ends_with(')')) {
return false;
}
if trimmed[..paren_pos].contains('=') {
return false;
}
let first_word = trimmed.split_whitespace().next().unwrap_or("");
let first_word = first_word.trim_start_matches('*');
!CONTROL_KEYWORDS.contains(&first_word)
}
pub fn extract_function_name(trimmed: &str, markers: &dyn FunctionDetectionMarkers) -> String {
for marker in markers.function_markers() {
if let Some(pos) = trimmed.find(marker) {
let after = &trimmed[pos + marker.len()..];
let name: String = after
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
return name;
}
}
}
if let Some(paren_pos) = trimmed.find('(') {
let before = trimmed[..paren_pos].trim();
if let Some(name) = before.split_whitespace().next_back() {
let name = name.trim_start_matches('*');
if !name.is_empty() {
return name.to_string();
}
}
}
"<anonymous>".to_string()
}
fn is_plain_assign(bytes: &[u8], pos: usize) -> bool {
let before = pos.checked_sub(1).map(|p| bytes[p]);
let after = bytes.get(pos + 1).copied();
!before.is_some_and(|b| b"=!<>+-*/%&|^:~".contains(&b))
&& !after.is_some_and(|b| b"=>~".contains(&b))
}
fn may_introduce_body(masked: &str) -> bool {
masked.trim_start().starts_with(')') || masked.trim_end().ends_with('=')
}
fn continues_expression(next: Option<&(usize, &str)>, base_indent: usize) -> bool {
next.is_some_and(|(_, line)| {
indent_level(line) > base_indent || line.trim_start().starts_with('{')
})
}
#[derive(Default)]
struct BodyScan {
brace_depth: isize,
paren_depth: isize,
found_open: bool,
expression_body: bool,
}
impl BodyScan {
fn read(&mut self, masked: &str, may_introduce: bool) {
let bytes = masked.as_bytes();
for (pos, &ch) in bytes.iter().enumerate() {
match ch {
b'{' => {
self.brace_depth += 1;
self.found_open = true;
}
b'}' => self.brace_depth -= 1,
b'(' => self.paren_depth += 1,
b')' => self.paren_depth -= 1,
b'=' if may_introduce && self.outside_signature() => {
self.expression_body |= is_plain_assign(bytes, pos);
}
_ => {}
}
}
}
fn outside_signature(&self) -> bool {
!self.found_open && self.paren_depth <= 0
}
fn ends(&self, next: Option<&(usize, &str)>, base_indent: usize) -> bool {
if self.expression_body {
self.brace_depth <= 0 && !continues_expression(next, base_indent)
} else {
self.found_open && self.brace_depth == 0
}
}
}
fn find_function_body<'a>(
code_lines: &[(usize, &'a str)],
start: usize,
line_comments: &[&str],
) -> (Vec<(usize, &'a str)>, usize) {
let mut scan = BodyScan::default();
let base_indent = indent_level(code_lines[start].1);
let mut func_code_lines: Vec<(usize, &str)> = Vec::new();
let mut j = start;
while j < code_lines.len() {
let (jidx, jline) = code_lines[j];
func_code_lines.push((jidx, jline));
let masked = mask_strings(jline, line_comments);
scan.read(&masked, j == start || may_introduce_body(&masked));
if scan.ends(code_lines.get(j + 1), base_indent) {
break;
}
j += 1;
}
(func_code_lines, j)
}
pub fn detect_function_bodies<'a>(
all_lines: &[String],
code_lines: &[(usize, &'a str)],
markers: &dyn FunctionDetectionMarkers,
) -> Vec<FunctionBody<'a>> {
if markers.brace_scoped() {
detect_with(
code_lines,
markers,
|trimmed| is_function_declaration(trimmed, markers),
|start| {
let (body, end) = find_function_body(code_lines, start, markers.line_comments());
(body, end + 1)
},
)
} else {
detect_with(
code_lines,
markers,
|trimmed| {
let mut function_markers = markers.function_markers().iter();
function_markers.any(|m| trimmed.starts_with(m))
},
|start| indented_body(all_lines, code_lines, start),
)
}
}
fn detect_with<'a>(
code_lines: &[(usize, &'a str)],
markers: &dyn FunctionDetectionMarkers,
is_declaration: impl Fn(&str) -> bool,
body_at: impl Fn(usize) -> (Vec<(usize, &'a str)>, usize),
) -> Vec<FunctionBody<'a>> {
let mut functions = Vec::new();
let mut i = 0;
while i < code_lines.len() {
let (line_idx, line) = code_lines[i];
let trimmed = line.trim();
if !is_declaration(trimmed) {
i += 1;
continue;
}
let (body, next) = body_at(i);
functions.push(FunctionBody {
name: extract_function_name(trimmed, markers),
start_line: line_idx + 1,
code_lines: body,
});
i = next;
}
functions
}
fn indented_body<'a>(
all_lines: &[String],
code_lines: &[(usize, &'a str)],
start: usize,
) -> (Vec<(usize, &'a str)>, usize) {
let base_indent = indent_level(code_lines[start].1);
let len = code_lines[start + 1..]
.iter()
.take_while(|(idx, line)| {
indent_level(&all_lines[*idx]) > base_indent || line.trim().is_empty()
})
.count();
let end = start + 1 + len;
(code_lines[start..end].to_vec(), end)
}
#[cfg(test)]
#[path = "detection_test.rs"]
mod tests;