use crate::labels::bare_method_name;
use crate::symbol::Lang;
use smallvec::SmallVec;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ContainerOp {
Store {
value_args: SmallVec<[usize; 2]>,
index_arg: Option<usize>,
},
Load { index_arg: Option<usize> },
Writeback { dest_arg: usize },
}
#[inline]
fn store(pos: usize) -> Option<ContainerOp> {
let mut v = SmallVec::new();
v.push(pos);
Some(ContainerOp::Store {
value_args: v,
index_arg: None,
})
}
#[inline]
fn store_indexed(val_pos: usize, idx_pos: usize) -> Option<ContainerOp> {
let mut v = SmallVec::new();
v.push(val_pos);
Some(ContainerOp::Store {
value_args: v,
index_arg: Some(idx_pos),
})
}
#[inline]
fn store2(a: usize, b: usize) -> Option<ContainerOp> {
let mut v = SmallVec::new();
v.push(a);
v.push(b);
Some(ContainerOp::Store {
value_args: v,
index_arg: None,
})
}
#[inline]
fn load() -> Option<ContainerOp> {
Some(ContainerOp::Load { index_arg: None })
}
#[inline]
fn load_indexed(idx_pos: usize) -> Option<ContainerOp> {
Some(ContainerOp::Load {
index_arg: Some(idx_pos),
})
}
pub fn classify_container_op(callee: &str, lang: Lang) -> Option<ContainerOp> {
let method = bare_method_name(callee);
match lang {
Lang::JavaScript | Lang::TypeScript => classify_js(method),
Lang::Python => classify_python(method),
Lang::Java => classify_java(method),
Lang::Go => classify_go(method, callee),
Lang::Ruby => classify_ruby(method),
Lang::Php => classify_php(method),
Lang::C | Lang::Cpp => classify_cpp(method),
Lang::Rust => classify_rust(method),
}
}
fn classify_js(method: &str) -> Option<ContainerOp> {
match method {
"push" | "unshift" => store(0),
"set" => store_indexed(1, 0),
"add" => store(0), "pop" | "shift" => load(),
"join" | "flat" | "concat" | "slice" | "toString" => load(),
"get" => load_indexed(0),
"values" | "keys" | "entries" => load(),
"__index_get__" => load_indexed(0),
"__index_set__" => store_indexed(1, 0),
_ => None,
}
}
fn classify_python(method: &str) -> Option<ContainerOp> {
match method {
"append" | "extend" => store(0),
"insert" => store_indexed(1, 0), "add" => store(0),
"update" => store(0),
"setdefault" => store2(0, 1), "pop" => load(),
"get" => load_indexed(0), "items" | "values" | "keys" => load(),
"join" => load(),
"__index_get__" => load_indexed(0),
"__index_set__" => store_indexed(1, 0),
_ => None,
}
}
fn classify_java(method: &str) -> Option<ContainerOp> {
match method {
"add" | "addAll" | "putAll" | "offer" | "push" => store(0),
"set" => store_indexed(1, 0),
"put" => store_indexed(1, 0),
"get" => load_indexed(0),
"poll" | "peek" | "remove" | "pop" => load(),
"stream" | "toArray" | "iterator" => load(),
_ => None,
}
}
fn classify_go(method: &str, callee: &str) -> Option<ContainerOp> {
if callee == "append" || method == "append" {
return store(1);
}
match method {
"Add" | "Set" | "Store" | "Put" => store(0),
"Get" | "Load" | "Pop" => load(),
"Decode" | "Unmarshal" => Some(ContainerOp::Writeback { dest_arg: 0 }),
"__index_get__" => load_indexed(0),
"__index_set__" => store_indexed(1, 0),
_ => None,
}
}
fn classify_ruby(method: &str) -> Option<ContainerOp> {
match method {
"push" | "append" | "unshift" | "store" | "<<" => store(0),
"pop" | "shift" | "first" | "last" | "fetch" | "join" => load(),
_ => None,
}
}
fn classify_php(method: &str) -> Option<ContainerOp> {
match method {
"array_push" => store(1), "array_pop" | "array_shift" | "current" | "next" | "reset" => load(),
_ => None,
}
}
fn classify_cpp(method: &str) -> Option<ContainerOp> {
match method {
"push_back" | "emplace_back" | "insert" | "emplace" | "push" | "assign" => store(0),
"insert_or_assign" => store_indexed(1, 0),
"front" | "back" | "pop_back" | "pop_front" | "top" | "find" | "count" | "data" => load(),
"at" => load_indexed(0),
_ => None,
}
}
fn classify_rust(method: &str) -> Option<ContainerOp> {
match method {
"push" | "insert" | "extend" => store(0),
"pop" | "first" | "last" | "iter" | "remove" => load(),
"get" => load_indexed(0),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn js_push_is_store() {
let op = classify_container_op("items.push", Lang::JavaScript);
assert!(matches!(op, Some(ContainerOp::Store { .. })));
}
#[test]
fn js_pop_is_load() {
let op = classify_container_op("arr.pop", Lang::JavaScript);
assert!(matches!(op, Some(ContainerOp::Load { .. })));
}
#[test]
fn js_join_is_load() {
let op = classify_container_op("items.join", Lang::JavaScript);
assert!(matches!(op, Some(ContainerOp::Load { .. })));
}
#[test]
fn python_append_is_store() {
let op = classify_container_op("commands.append", Lang::Python);
assert!(matches!(op, Some(ContainerOp::Store { .. })));
}
#[test]
fn java_add_is_store() {
let op = classify_container_op("list.add", Lang::Java);
assert!(matches!(op, Some(ContainerOp::Store { .. })));
}
#[test]
fn go_append_is_store() {
let op = classify_container_op("append", Lang::Go);
assert!(matches!(op, Some(ContainerOp::Store { .. })));
}
#[test]
fn go_decode_is_writeback_dest_arg_zero() {
match classify_container_op("decoder.Decode", Lang::Go) {
Some(ContainerOp::Writeback { dest_arg }) => assert_eq!(dest_arg, 0),
other => panic!("expected Writeback {{ dest_arg: 0 }}, got {other:?}"),
}
}
#[test]
fn go_unmarshal_is_writeback_dest_arg_zero() {
match classify_container_op("hdr.Unmarshal", Lang::Go) {
Some(ContainerOp::Writeback { dest_arg }) => assert_eq!(dest_arg, 0),
other => panic!("expected Writeback {{ dest_arg: 0 }}, got {other:?}"),
}
}
#[test]
fn js_decode_is_not_writeback() {
assert!(classify_container_op("decoder.Decode", Lang::JavaScript).is_none());
assert!(classify_container_op("decoder.Decode", Lang::Python).is_none());
}
#[test]
fn unknown_method_is_none() {
assert!(classify_container_op("obj.frobnicate", Lang::JavaScript).is_none());
}
#[test]
fn rust_push_is_store() {
let op = classify_container_op("vec.push", Lang::Rust);
assert!(matches!(op, Some(ContainerOp::Store { .. })));
}
#[test]
fn store_value_args_correct() {
if let Some(ContainerOp::Store {
value_args,
index_arg,
}) = classify_container_op("map.set", Lang::JavaScript)
{
assert_eq!(value_args.as_slice(), &[1]);
assert_eq!(index_arg, Some(0));
} else {
panic!("expected Store");
}
if let Some(ContainerOp::Store {
value_args,
index_arg,
}) = classify_container_op("arr.push", Lang::JavaScript)
{
assert_eq!(value_args.as_slice(), &[0]);
assert_eq!(index_arg, None);
} else {
panic!("expected Store");
}
}
#[test]
fn load_index_arg_correct() {
if let Some(ContainerOp::Load { index_arg }) =
classify_container_op("map.get", Lang::JavaScript)
{
assert_eq!(index_arg, Some(0));
} else {
panic!("expected Load");
}
if let Some(ContainerOp::Load { index_arg }) =
classify_container_op("arr.pop", Lang::JavaScript)
{
assert_eq!(index_arg, None);
} else {
panic!("expected Load");
}
}
#[test]
fn cpp_push_back_is_store() {
let op = classify_container_op("v.push_back", Lang::Cpp);
match op {
Some(ContainerOp::Store {
value_args,
index_arg,
}) => {
assert_eq!(value_args.as_slice(), &[0]);
assert_eq!(index_arg, None);
}
_ => panic!("expected Store"),
}
}
#[test]
fn cpp_assign_is_store() {
let op = classify_container_op("v.assign", Lang::Cpp);
assert!(matches!(op, Some(ContainerOp::Store { .. })));
}
#[test]
fn cpp_insert_or_assign_indexes_value() {
match classify_container_op("m.insert_or_assign", Lang::Cpp) {
Some(ContainerOp::Store {
value_args,
index_arg,
}) => {
assert_eq!(value_args.as_slice(), &[1]);
assert_eq!(index_arg, Some(0));
}
other => panic!("expected indexed Store, got {other:?}"),
}
}
#[test]
fn cpp_find_count_data_are_load() {
for callee in ["m.find", "m.count", "v.data"] {
assert!(
matches!(
classify_container_op(callee, Lang::Cpp),
Some(ContainerOp::Load { .. })
),
"{callee} should be a Load",
);
}
}
#[test]
fn cpp_at_is_indexed_load() {
match classify_container_op("v.at", Lang::Cpp) {
Some(ContainerOp::Load { index_arg }) => assert_eq!(index_arg, Some(0)),
other => panic!("expected indexed Load, got {other:?}"),
}
}
#[test]
fn synth_index_get_classified_as_indexed_load_js_py_go() {
for lang in [Lang::JavaScript, Lang::TypeScript, Lang::Python, Lang::Go] {
match classify_container_op("__index_get__", lang) {
Some(ContainerOp::Load { index_arg }) => {
assert_eq!(index_arg, Some(0), "{lang:?} should mark idx arg=0");
}
other => panic!("{lang:?}: expected indexed Load, got {other:?}"),
}
}
}
#[test]
fn synth_index_set_classified_as_indexed_store_js_py_go() {
for lang in [Lang::JavaScript, Lang::TypeScript, Lang::Python, Lang::Go] {
match classify_container_op("__index_set__", lang) {
Some(ContainerOp::Store {
value_args,
index_arg,
}) => {
assert_eq!(
value_args.as_slice(),
&[1],
"{lang:?} value arg should be 1"
);
assert_eq!(index_arg, Some(0), "{lang:?} index arg should be 0");
}
other => panic!("{lang:?}: expected indexed Store, got {other:?}"),
}
}
}
}