use std::str::FromStr;
use std::sync::OnceLock;
use backhopper_erlang_scan::{
ScannedArgs, ScannedList, is_bare_atom, scan_list_elements, scan_top_level_args,
};
use regex::Regex;
use crate::compat::call_sites::{
BodyRun, body_runs_from_context, body_runs_with, call_re, line_context, run_line_at,
};
use crate::model::names::{Arity, FunctionName, Mfa, ModuleName};
use crate::model::symbol::LineClass;
use crate::model::verdict::IndirectCallForm;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Syntax {
Erlang,
Elixir,
}
impl Syntax {
fn parse_atom(self, raw: &str) -> Option<String> {
match self {
Self::Erlang => parse_atom(raw),
Self::Elixir => parse_elixir_atom(raw),
}
}
fn fun_arity(self, raw: Option<&&str>) -> Option<Arity> {
match self {
Self::Erlang => raw.and_then(|a| fun_arity(a)),
Self::Elixir => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndirectCallSite {
pub mfa: Mfa,
pub via: IndirectCallForm,
pub line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WithheldIndirectCall {
pub module: ModuleName,
pub function: FunctionName,
pub line: u32,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct IndirectExtraction {
pub sites: Vec<IndirectCallSite>,
pub withheld_dynamic: Vec<WithheldIndirectCall>,
}
impl IndirectExtraction {
fn withhold(&mut self, module: ModuleName, function: FunctionName, line: u32) {
self.withheld_dynamic.push(WithheldIndirectCall {
module,
function,
line,
});
}
}
struct FormSpec {
via: IndirectCallForm,
module_pos: usize,
function_pos: usize,
arity: AritySource,
}
enum AritySource {
IntegerLiteral(usize),
FunExpression(usize),
ArgumentList(usize),
}
fn form_spec(module: &str, function: &str, argc: usize) -> Option<FormSpec> {
let spec = |via, module_pos, function_pos, arity| FormSpec {
via,
module_pos,
function_pos,
arity,
};
match (module, function, argc) {
("meck", "expect", 3) => Some(spec(
IndirectCallForm::MeckExpect,
0,
1,
AritySource::FunExpression(2),
)),
("meck", "expect", 4) => Some(spec(
IndirectCallForm::MeckExpect,
0,
1,
AritySource::IntegerLiteral(2),
)),
("rpc", "call", 4 | 5) => Some(spec(
IndirectCallForm::RpcCall,
1,
2,
AritySource::ArgumentList(3),
)),
("rabbit_misc", "rpc_call", 4 | 5) => Some(spec(
IndirectCallForm::RabbitMiscRpcCall,
1,
2,
AritySource::ArgumentList(3),
)),
("erpc", "call", 4 | 5) => Some(spec(
IndirectCallForm::ErpcCall,
1,
2,
AritySource::ArgumentList(3),
)),
("rabbit_ct_broker_helpers", "rpc", 4) | ("rabbit_ct_broker_helpers", "rpc_all", 4 | 5) => {
Some(spec(
IndirectCallForm::CtBrokerHelperRpc,
1,
2,
AritySource::ArgumentList(3),
))
}
("rabbit_ct_broker_helpers", "rpc", 5 | 6) => Some(spec(
IndirectCallForm::CtBrokerHelperRpc,
2,
3,
AritySource::ArgumentList(4),
)),
_ => None,
}
}
pub fn extract_indirect_calls(src: &str, line_map: &[u32]) -> IndirectExtraction {
extract_indirect_calls_with_context(src, line_map, &line_context(src))
}
pub fn extract_indirect_calls_with_context(
src: &str,
line_map: &[u32],
ctx: &[LineClass],
) -> IndirectExtraction {
let mut out = IndirectExtraction::default();
for run in body_runs_from_context(src, line_map, ctx) {
for caps in call_re().captures_iter(&run.text) {
let group = caps.get(0).expect("capture");
let ScannedArgs::Terminated { args, .. } =
scan_top_level_args(&run.text[group.end()..])
else {
continue;
};
let Some(spec) = form_spec(&caps[1], &caps[2], args.len()) else {
continue;
};
let line = run_line_at(&run.line_starts, group.start());
read_reference(&spec, &args, line, Syntax::Erlang, &mut out);
}
}
out
}
pub fn extract_indirect_calls_elixir(src: &str, line_map: &[u32]) -> IndirectExtraction {
let mut out = IndirectExtraction::default();
for run in elixir_body_runs(src, line_map) {
for caps in elixir_call_re().captures_iter(&run.text) {
let group = caps.get(0).expect("capture");
let ScannedArgs::Terminated { args, .. } =
scan_top_level_args(&run.text[group.end()..])
else {
continue;
};
let Some(spec) = form_spec(&caps[1], &caps[2], args.len()) else {
continue;
};
let line = run_line_at(&run.line_starts, group.start());
read_reference(&spec, &args, line, Syntax::Elixir, &mut out);
}
}
out
}
fn read_reference(
spec: &FormSpec,
args: &[&str],
line: u32,
syntax: Syntax,
out: &mut IndirectExtraction,
) {
let (Some(module), Some(function)) = (
args.get(spec.module_pos).and_then(|a| syntax.parse_atom(a)),
args.get(spec.function_pos)
.and_then(|a| syntax.parse_atom(a)),
) else {
return;
};
let (Ok(module), Ok(function)) = (
ModuleName::from_str(&module),
FunctionName::from_str(&function),
) else {
return;
};
let arity = match &spec.arity {
AritySource::IntegerLiteral(pos) => integer_arity(args.get(*pos)),
AritySource::FunExpression(pos) => syntax.fun_arity(args.get(*pos)),
AritySource::ArgumentList(pos) => match list_elements(args.get(*pos)) {
Some(elements) => {
if module.as_str() == "meck" && function.as_str() == "expect" {
if let Some(inner) = form_spec("meck", "expect", elements.len()) {
let elements: Vec<&str> = elements.iter().map(String::as_str).collect();
read_reference(&inner, &elements, line, syntax, out);
} else {
out.withhold(module, function, line);
}
return;
}
Arity::try_from(elements.len()).ok()
}
None => None,
},
};
match arity {
Some(arity) => out.sites.push(IndirectCallSite {
mfa: Mfa::new(module, function, arity),
via: spec.via,
line,
}),
None => out.withhold(module, function, line),
}
}
fn parse_atom(raw: &str) -> Option<String> {
let raw = raw.trim();
if let Some(inner) = raw.strip_prefix('\'') {
let inner = inner.strip_suffix('\'')?;
if inner.is_empty() || inner.contains('\'') {
return None;
}
return Some(inner.to_owned());
}
if is_bare_atom(raw) {
Some(raw.to_owned())
} else {
None
}
}
fn parse_elixir_atom(raw: &str) -> Option<String> {
let rest = raw.trim().strip_prefix(':')?;
for quote in ['"', '\''] {
if let Some(inner) = rest.strip_prefix(quote) {
let inner = inner.strip_suffix(quote)?;
if inner.contains(quote) {
return None;
}
return Some(inner.to_owned());
}
}
let mut chars = rest.chars();
let first = chars.next()?;
if (first.is_ascii_lowercase() || first == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '@')
{
Some(rest.to_owned())
} else {
None
}
}
fn elixir_call_re() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| {
Regex::new(r":([a-z][a-zA-Z0-9_@]*)\.([a-z][a-zA-Z0-9_@]*)\s*\(").expect("regex")
})
}
fn elixir_body_runs(src: &str, line_map: &[u32]) -> Vec<BodyRun> {
body_runs_with(src, line_map, strip_elixir_line_comment, |_| true)
}
fn strip_elixir_line_comment(line: &str) -> &str {
let bytes = line.as_bytes();
let mut in_str = false;
let mut in_charlist = false;
let mut prev_ident = false;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if in_str || in_charlist {
match b {
b'\\' => i += 1,
b'"' if in_str => in_str = false,
b'\'' if in_charlist => in_charlist = false,
_ => {}
}
i += 1;
prev_ident = false;
continue;
}
match b {
b'"' => in_str = true,
b'\'' => in_charlist = true,
b'?' if !prev_ident => {
i += 1;
if bytes.get(i) == Some(&b'\\') {
i += 1;
}
i += 1;
prev_ident = false;
continue;
}
b'#' => return &line[..i],
_ => {}
}
prev_ident = b.is_ascii_alphanumeric() || b == b'_';
i += 1;
}
line
}
fn integer_arity(raw: Option<&&str>) -> Option<Arity> {
let raw = raw?.trim();
let value: usize = raw.parse().ok()?;
Arity::try_from(value).ok()
}
fn fun_arity(raw: &str) -> Option<Arity> {
let rest = raw.trim().strip_prefix("fun")?;
if !rest.starts_with('(') && !rest.starts_with(char::is_whitespace) {
return None;
}
let rest = rest.trim_start();
if let Some(after_paren) = rest.strip_prefix('(') {
let ScannedArgs::Terminated { args, .. } = scan_top_level_args(after_paren) else {
return None;
};
return Arity::try_from(args.len()).ok();
}
let (_, digits) = rest.rsplit_once('/')?;
let value: usize = digits.trim().parse().ok()?;
Arity::try_from(value).ok()
}
fn list_elements(raw: Option<&&str>) -> Option<Vec<String>> {
let raw = raw?.trim();
let after_bracket = raw.strip_prefix('[')?;
match scan_list_elements(after_bracket) {
ScannedList::Terminated { elements, consumed }
if after_bracket[consumed..].trim().is_empty() =>
{
Some(elements.into_iter().map(str::to_owned).collect())
}
ScannedList::Terminated { .. } | ScannedList::ImproperTail | ScannedList::Unterminated => {
None
}
}
}