use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crate::path_search::search::definition_search;
use crate::path_search::utils::get_backward_paths;
use crate::path_search::{Path, RECURSION_LIMIT};
use crate::query::{adj_ast, match_ast_group_d, pred_ast};
use crate::syntax::fold::{fold, fold_leaf, strip_quotes, Folded};
use crate::syntax::{SyntaxGraph, SyntaxNode};
use crate::{CodeGraph, NodeId};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CondBranch {
TrueId,
FalseId,
}
fn char_at(text: &str, index: i64) -> Option<char> {
if index >= 0 {
usize::try_from(index)
.ok()
.and_then(|i| text.chars().nth(i))
} else {
index
.checked_neg()
.and_then(|positive| usize::try_from(positive).ok())
.and_then(|i| i.checked_sub(1))
.and_then(|i| text.chars().rev().nth(i))
}
}
#[must_use]
pub fn search_value(graph: &SyntaxGraph, n_id: NodeId, symbol: &str) -> Option<NodeId> {
adj_ast(graph, n_id, None, &[]).into_iter().find(|c_id| {
matches!(
graph.nodes.get(c_id),
Some(SyntaxNode::SwitchSection { case_expression })
if strip_quotes(case_expression) == symbol
)
})
}
#[must_use]
pub fn guess_char(graph: &SyntaxGraph, path: &Path, n_id: NodeId) -> Option<String> {
let node = graph.nodes.get(&n_id)?;
if let Some(val_id) = node.value_id() {
return graph
.nodes
.get(&val_id)
.and_then(SyntaxNode::value)
.map(String::from);
}
let SyntaxNode::MethodInvocation {
expression,
object_id,
arguments_id,
..
} = node
else {
return None;
};
if expression != "charAt" {
return None;
}
let obj_symbol = object_id
.and_then(|obj_id| graph.nodes.get(&obj_id))
.and_then(SyntaxNode::symbol)?;
let val_id = definition_search(graph, path, obj_symbol)?;
let args_id = (*arguments_id)?;
let char_pos_id = adj_ast(graph, args_id, None, &[]).first().copied()?;
let char_pos = graph
.nodes
.get(&char_pos_id)
.and_then(SyntaxNode::value)?
.parse::<i64>()
.ok()
.filter(|position| *position != 0)?;
let guess_val_id = graph.nodes.get(&val_id).and_then(SyntaxNode::value_id)?;
let guess_value = graph
.nodes
.get(&guess_val_id)
.and_then(SyntaxNode::value)
.filter(|value| !value.is_empty())?;
char_at(strip_quotes(guess_value), char_pos).map(String::from)
}
#[must_use]
pub fn get_symbol_assignment(graph: &SyntaxGraph, n_id: NodeId, symbol: &str) -> Option<String> {
for path in get_backward_paths(graph, n_id, Some(RECURSION_LIMIT)) {
let value = definition_search(graph, &path, symbol)
.and_then(|var_def_id| graph.nodes.get(&var_def_id))
.and_then(SyntaxNode::value_id)
.and_then(|val_id| guess_char(graph, &path, val_id))
.filter(|value| !value.is_empty());
if let Some(value) = value {
return Some(String::from(strip_quotes(&value)));
}
}
None
}
#[must_use]
pub fn get_deterministic_path_id(graph: &SyntaxGraph, n_id: NodeId) -> Option<NodeId> {
let parent_id = pred_ast(graph, n_id, None).first().copied()?;
let val_id = graph.nodes.get(&parent_id).and_then(SyntaxNode::value_id)?;
let symbol = graph
.nodes
.get(&val_id)
.and_then(SyntaxNode::symbol)
.filter(|symbol| !symbol.is_empty())?;
let var_definition =
get_symbol_assignment(graph, parent_id, symbol).filter(|value| !value.is_empty())?;
search_value(graph, n_id, &var_definition)
}
#[must_use]
pub fn get_return_literal(graph: &SyntaxGraph, n_id: NodeId, symbol: &str) -> Option<String> {
let mut literal_values: BTreeSet<String> = BTreeSet::new();
for path in get_backward_paths(graph, n_id, Some(RECURSION_LIMIT)) {
let return_value = definition_search(graph, &path, symbol)
.and_then(|var_def_id| graph.nodes.get(&var_def_id))
.and_then(SyntaxNode::value_id)
.and_then(|val_id| {
graph
.nodes
.get(&val_id)
.filter(|node| matches!(node, SyntaxNode::Literal { .. }))
})
.and_then(SyntaxNode::value)
.filter(|value| !value.is_empty())?;
literal_values.insert(String::from(strip_quotes(return_value)));
}
literal_values.last().cloned()
}
#[must_use]
pub fn search_symbol_val_id(graph: &SyntaxGraph, n_id: NodeId) -> Option<NodeId> {
let symbol = graph.nodes.get(&n_id).and_then(SyntaxNode::symbol)?;
for path in get_backward_paths(graph, n_id, Some(RECURSION_LIMIT)) {
if let Some(node) = definition_search(graph, &path, symbol)
.and_then(|var_def_id| graph.nodes.get(&var_def_id))
{
if matches!(node, SyntaxNode::VariableDeclaration { .. }) {
return node.value_id();
}
}
}
None
}
#[must_use]
pub fn get_condition_deterministic_result(
graph: &mut SyntaxGraph,
cond_id: NodeId,
) -> Option<CondBranch> {
let symbol_ids = if graph.label_type(cond_id) == Some("SymbolLookup") {
vec![cond_id]
} else {
match_ast_group_d(&*graph, cond_id, "SymbolLookup", Some(-1))
};
let mut resolved: BTreeMap<NodeId, Folded> = BTreeMap::new();
for var_id in symbol_ids {
let (var_value, var_type) = search_symbol_val_id(graph, var_id).and_then(|val_id| {
match graph.nodes.get(&val_id) {
Some(SyntaxNode::Literal { value, value_type }) if !value.is_empty() => {
Some((value.clone(), value_type.clone()))
}
_ => None,
}
})?;
if let Some(folded) = fold_leaf(&var_value, &var_type) {
resolved.insert(var_id, folded);
}
let Some(SyntaxNode::SymbolLookup { value, .. }) = graph.nodes.get_mut(&var_id) else {
return None;
};
*value = Some(var_value);
}
match fold(graph, &resolved, cond_id)? {
Folded::Truth(true) => Some(CondBranch::TrueId),
Folded::Truth(false) => Some(CondBranch::FalseId),
Folded::Int(_) | Folded::Float(_) | Folded::Text(_) => None,
}
}
fn get_element_in_hashmap(
graph: &SyntaxGraph,
path: &Path,
var_name: &str,
access_key: &str,
) -> Option<NodeId> {
for &n_id in path {
let Some(SyntaxNode::MethodInvocation {
expression,
object_id: Some(object_id),
arguments_id: Some(arguments_id),
..
}) = graph.nodes.get(&n_id)
else {
continue;
};
if expression != "put"
|| graph.nodes.get(object_id).and_then(SyntaxNode::symbol) != Some(var_name)
{
continue;
}
let arg_ids = adj_ast(graph, *arguments_id, None, &[]);
if arg_ids.len() < 2 {
continue;
}
let key_matches = arg_ids
.first()
.and_then(|first| graph.nodes.get(first))
.and_then(SyntaxNode::value)
== Some(access_key);
if key_matches {
return arg_ids.get(1).copied();
}
}
None
}
fn element_at(d_nodes: &[NodeId], access_val: i64) -> Option<NodeId> {
let magnitude = usize::try_from(access_val.unsigned_abs()).ok()?;
if d_nodes.is_empty() || d_nodes.len() < magnitude {
return None;
}
if access_val >= 0 {
d_nodes.get(magnitude).copied()
} else {
d_nodes.get(d_nodes.len().checked_sub(magnitude)?).copied()
}
}
fn remove_replayed_element(
graph: &SyntaxGraph,
d_nodes: &mut Vec<NodeId>,
index_value: &str,
) -> Option<()> {
let Ok(index) = index_value.parse::<i64>() else {
let matching = d_nodes.iter().position(|element| {
graph.nodes.get(element).and_then(SyntaxNode::value) == Some(index_value)
});
if let Some(position) = matching {
d_nodes.remove(position);
}
return Some(());
};
let magnitude = usize::try_from(index.unsigned_abs()).ok()?;
let position = if index >= 0 {
magnitude
} else {
d_nodes.len().checked_sub(magnitude)?
};
if position >= d_nodes.len() {
return None;
}
d_nodes.remove(position);
Some(())
}
fn get_element_in_array(
graph: &SyntaxGraph,
path: &Path,
var_name: &str,
access_val: i64,
) -> Option<NodeId> {
let mut d_nodes: Vec<NodeId> = Vec::new();
for &n_id in path.iter().rev() {
let Some(SyntaxNode::MethodInvocation {
expression,
object_id: Some(object_id),
arguments_id: Some(arguments_id),
..
}) = graph.nodes.get(&n_id)
else {
continue;
};
if graph.nodes.get(object_id).and_then(SyntaxNode::symbol) != Some(var_name) {
continue;
}
let Some(arg_id) = adj_ast(graph, *arguments_id, None, &[]).first().copied() else {
continue;
};
match expression.as_str() {
"add" | "push" => d_nodes.push(arg_id),
"remove" => {
let index_value = graph
.nodes
.get(&arg_id)
.and_then(SyntaxNode::value)
.filter(|value| !value.is_empty())?;
remove_replayed_element(graph, &mut d_nodes, index_value)?;
}
_ => {}
}
}
element_at(&d_nodes, access_val)
}
fn search_data_element(graph: &SyntaxGraph, method_id: NodeId) -> Option<NodeId> {
let Some(SyntaxNode::MethodInvocation {
object_id: Some(object_id),
arguments_id: Some(arguments_id),
..
}) = graph.nodes.get(&method_id)
else {
return None;
};
let var_name = graph
.nodes
.get(object_id)
.and_then(SyntaxNode::symbol)
.filter(|symbol| !symbol.is_empty())?;
let args_ids = adj_ast(graph, *arguments_id, None, &[]);
let [access_id] = args_ids.as_slice() else {
return None;
};
let Some(SyntaxNode::Literal { value, value_type }) = graph.nodes.get(access_id) else {
return None;
};
let access_val = match value_type.as_str() {
"number" => Some(value.parse::<i64>().ok()?),
"string" => None,
_ => return None,
};
let path = get_backward_paths(graph, method_id, Some(RECURSION_LIMIT))
.into_iter()
.next()?;
access_val.map_or_else(
|| get_element_in_hashmap(graph, &path, var_name, value),
|index| get_element_in_array(graph, &path, var_name, index),
)
}
fn get_node_access_method(graph: &SyntaxGraph, n_id: NodeId) -> Option<NodeId> {
let method_id = match graph.nodes.get(&n_id) {
Some(SyntaxNode::VariableDeclaration {
value_id: Some(val_id),
..
}) => *val_id,
_ => n_id,
};
matches!(
graph.nodes.get(&method_id),
Some(SyntaxNode::MethodInvocation { expression, .. }) if expression == "get"
)
.then_some(method_id)
}
fn rewire_assignment_value(
graph: &mut SyntaxGraph,
n_id: NodeId,
old_value_id: NodeId,
c_id: NodeId,
detached_parent: NodeId,
) {
graph.remove_edge(n_id, old_value_id);
graph.remove_edge(detached_parent, c_id);
if let Some(SyntaxNode::Assignment { value_id, .. }) = graph.nodes.get_mut(&n_id) {
*value_id = Some(c_id);
}
graph.add_ast_edge(n_id, c_id);
}
pub fn adjust_assignment_ast_edges(graph: &mut SyntaxGraph, n_id: NodeId) {
let Some(SyntaxNode::Assignment {
value_id: Some(value_id),
..
}) = graph.nodes.get(&n_id)
else {
return;
};
let value_id = *value_id;
if let Some(SyntaxNode::TernaryOperation {
condition_id,
true_id,
false_id,
}) = graph.nodes.get(&value_id)
{
let (condition_id, true_id, false_id) = (*condition_id, *true_id, *false_id);
if let Some(branch) = get_condition_deterministic_result(graph, condition_id) {
let c_id = match branch {
CondBranch::TrueId => true_id,
CondBranch::FalseId => false_id,
};
rewire_assignment_value(graph, n_id, value_id, c_id, value_id);
}
}
if let Some(c_id) = get_node_access_method(graph, value_id)
.and_then(|method_id| search_data_element(graph, method_id))
{
let Some(parent_id) = pred_ast(graph, c_id, None).first().copied() else {
return;
};
rewire_assignment_value(graph, n_id, value_id, c_id, parent_id);
}
}
pub fn adjust_return_value(graph: &mut SyntaxGraph, n_id: NodeId) {
let resolved = graph
.nodes
.get(&n_id)
.and_then(SyntaxNode::value_id)
.and_then(|val_id| {
let symbol = graph
.nodes
.get(&val_id)
.and_then(SyntaxNode::symbol)
.filter(|symbol| !symbol.is_empty())?;
let ret_val =
get_return_literal(graph, val_id, symbol).filter(|value| !value.is_empty())?;
Some((val_id, ret_val))
});
if let Some((val_id, ret_val)) = resolved {
graph.add_node(
val_id,
SyntaxNode::Literal {
value: ret_val,
value_type: String::new(),
},
);
}
}
#[cfg(test)]
mod tests {
use super::{
adjust_assignment_ast_edges, adjust_return_value, get_condition_deterministic_result,
get_return_literal, guess_char, CondBranch,
};
use crate::syntax::{SyntaxGraph, SyntaxNode};
use crate::CodeGraph;
use crate::NodeId;
use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec;
fn literal(value: &str, value_type: &str) -> SyntaxNode {
SyntaxNode::Literal {
value: value.to_owned(),
value_type: value_type.to_owned(),
}
}
fn symbol_lookup(symbol: &str) -> SyntaxNode {
SyntaxNode::SymbolLookup {
symbol: symbol.to_owned(),
symbol_scope: None,
value: None,
}
}
fn variable_declaration(name: &str, value_id: NodeId) -> SyntaxNode {
SyntaxNode::VariableDeclaration {
variable: name.to_owned(),
variable_type: None,
value_id: Some(value_id),
variable_id: None,
access_modifier: None,
}
}
fn char_at_graph(position: &str) -> SyntaxGraph {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), variable_declaration("s", NodeId(2)));
graph.add_node(NodeId(2), literal("\"hello\"", "string"));
graph.add_node(
NodeId(3),
SyntaxNode::MethodInvocation {
expression: String::from("charAt"),
object: None,
symbol_scope: None,
expression_id: None,
arguments_id: Some(NodeId(5)),
object_id: Some(NodeId(4)),
block_id: None,
receiver_type_fqn: None,
},
);
graph.add_node(NodeId(4), symbol_lookup("s"));
graph.add_node(NodeId(5), SyntaxNode::ArgumentList);
graph.add_node(NodeId(6), literal(position, "number"));
graph.add_ast_edge(NodeId(5), NodeId(6));
graph
}
#[test]
fn guess_char_resolves_a_char_at_call_over_the_definition() {
let graph = char_at_graph("2");
let path = vec![NodeId(1)];
assert_eq!(
guess_char(&graph, &path, NodeId(3)),
Some(String::from("l"))
);
}
#[test]
fn guess_char_mirrors_the_python_zero_index_falsiness() {
let graph = char_at_graph("0");
let path = vec![NodeId(1)];
assert_eq!(guess_char(&graph, &path, NodeId(3)), None);
}
#[test]
fn get_return_literal_takes_the_greatest_value_across_paths() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), variable_declaration("x", NodeId(2)));
graph.add_node(NodeId(2), literal("\"a\"", "string"));
graph.add_node(NodeId(3), variable_declaration("x", NodeId(4)));
graph.add_node(NodeId(4), literal("\"b\"", "string"));
graph.add_node(NodeId(5), SyntaxNode::ExecutionBlock);
graph.add_cfg_edge(NodeId(1), NodeId(5));
graph.add_cfg_edge(NodeId(3), NodeId(5));
assert_eq!(
get_return_literal(&graph, NodeId(5), "x"),
Some(String::from("b"))
);
}
fn binary_operation(operator: &str, left_id: NodeId, right_id: NodeId) -> SyntaxNode {
SyntaxNode::BinaryOperation {
operator: operator.to_owned(),
left_id: Some(left_id),
right_id: Some(right_id),
}
}
#[test]
fn resolves_a_symbol_to_its_literal_definition_before_folding() {
let mut graph = SyntaxGraph::new();
graph.add_node(NodeId(1), variable_declaration("x", NodeId(2)));
graph.add_node(NodeId(2), literal("1", "number"));
graph.add_node(NodeId(3), binary_operation("==", NodeId(4), NodeId(5)));
graph.add_node(NodeId(4), symbol_lookup("x"));
graph.add_node(NodeId(5), literal("1", "number"));
graph.add_ast_edge(NodeId(3), NodeId(4));
graph.add_ast_edge(NodeId(3), NodeId(5));
graph.add_cfg_edge(NodeId(1), NodeId(3));
assert_eq!(
get_condition_deterministic_result(&mut graph, NodeId(3)),
Some(CondBranch::TrueId)
);
assert_eq!(
graph.nodes.get(&NodeId(4)).and_then(SyntaxNode::value),
Some("1")
);
}
#[test]
fn adjust_assignment_rewires_a_deterministic_ternary() {
let mut graph = SyntaxGraph::new();
graph.add_node(
NodeId(1),
SyntaxNode::Assignment {
variable_id: NodeId(6),
value_id: Some(NodeId(2)),
operator: None,
},
);
graph.add_node(
NodeId(2),
SyntaxNode::TernaryOperation {
condition_id: NodeId(3),
true_id: NodeId(4),
false_id: NodeId(5),
},
);
graph.add_node(NodeId(3), literal("true", "boolean"));
graph.add_node(NodeId(4), literal("\"a\"", "string"));
graph.add_node(NodeId(5), literal("\"b\"", "string"));
graph.add_node(NodeId(6), symbol_lookup("x"));
graph.add_ast_edge(NodeId(1), NodeId(2));
graph.add_ast_edge(NodeId(2), NodeId(3));
graph.add_ast_edge(NodeId(2), NodeId(4));
graph.add_ast_edge(NodeId(2), NodeId(5));
adjust_assignment_ast_edges(&mut graph, NodeId(1));
assert_eq!(graph.children(NodeId(1)), [NodeId(4)]);
assert_eq!(graph.children(NodeId(2)), [NodeId(3), NodeId(5)]);
assert_eq!(
graph.nodes.get(&NodeId(1)).and_then(SyntaxNode::value_id),
Some(NodeId(4))
);
}
fn collection_access_graph(access: (&str, &str), stored_key: &str) -> SyntaxGraph {
let mut graph = SyntaxGraph::new();
graph.add_node(
NodeId(1),
SyntaxNode::Assignment {
variable_id: NodeId(12),
value_id: Some(NodeId(2)),
operator: None,
},
);
graph.add_node(
NodeId(2),
SyntaxNode::MethodInvocation {
expression: String::from("get"),
object: None,
symbol_scope: None,
expression_id: None,
arguments_id: Some(NodeId(4)),
object_id: Some(NodeId(3)),
block_id: None,
receiver_type_fqn: None,
},
);
graph.add_node(NodeId(3), symbol_lookup("items"));
graph.add_node(NodeId(4), SyntaxNode::ArgumentList);
graph.add_node(NodeId(5), literal(access.0, access.1));
graph.add_node(
NodeId(6),
SyntaxNode::MethodInvocation {
expression: String::from(if access.1 == "number" { "add" } else { "put" }),
object: None,
symbol_scope: None,
expression_id: None,
arguments_id: Some(NodeId(8)),
object_id: Some(NodeId(7)),
block_id: None,
receiver_type_fqn: None,
},
);
graph.add_node(NodeId(7), symbol_lookup("items"));
graph.add_node(NodeId(8), SyntaxNode::ArgumentList);
graph.add_node(NodeId(9), literal(stored_key, "string"));
graph.add_node(NodeId(10), symbol_lookup("stored"));
graph.add_node(NodeId(12), symbol_lookup("x"));
graph.add_ast_edge(NodeId(1), NodeId(2));
graph.add_ast_edge(NodeId(4), NodeId(5));
graph.add_ast_edge(NodeId(8), NodeId(9));
graph.add_ast_edge(NodeId(8), NodeId(10));
graph.add_cfg_edge(NodeId(6), NodeId(1));
graph
}
#[test]
fn adjust_assignment_rewires_a_replayed_list_access() {
let mut graph = collection_access_graph(("0", "number"), "\"first\"");
adjust_assignment_ast_edges(&mut graph, NodeId(1));
assert_eq!(graph.children(NodeId(1)), [NodeId(9)]);
assert_eq!(graph.children(NodeId(8)), [NodeId(10)]);
assert_eq!(
graph.nodes.get(&NodeId(1)).and_then(SyntaxNode::value_id),
Some(NodeId(9))
);
}
#[test]
fn adjust_assignment_rewires_a_replayed_map_access() {
let mut graph = collection_access_graph(("\"k\"", "string"), "\"k\"");
adjust_assignment_ast_edges(&mut graph, NodeId(1));
assert_eq!(graph.children(NodeId(1)), [NodeId(10)]);
assert_eq!(graph.children(NodeId(8)), [NodeId(9)]);
assert_eq!(
graph.nodes.get(&NodeId(1)).and_then(SyntaxNode::value_id),
Some(NodeId(10))
);
}
#[test]
fn an_unmodelable_remove_call_aborts_the_replay() {
let mut graph = collection_access_graph(("0", "number"), "\"first\"");
graph.add_node(
NodeId(13),
SyntaxNode::MethodInvocation {
expression: String::from("remove"),
object: None,
symbol_scope: None,
expression_id: None,
arguments_id: Some(NodeId(15)),
object_id: Some(NodeId(14)),
block_id: None,
receiver_type_fqn: None,
},
);
graph.add_node(NodeId(14), symbol_lookup("items"));
graph.add_node(NodeId(15), SyntaxNode::ArgumentList);
graph.add_node(NodeId(16), symbol_lookup("victim"));
graph.add_ast_edge(NodeId(15), NodeId(16));
graph.remove_edge(NodeId(6), NodeId(1));
graph.add_cfg_edge(NodeId(6), NodeId(13));
graph.add_cfg_edge(NodeId(13), NodeId(1));
adjust_assignment_ast_edges(&mut graph, NodeId(1));
assert_eq!(graph.children(NodeId(1)), [NodeId(2)]);
assert_eq!(
graph.nodes.get(&NodeId(1)).and_then(SyntaxNode::value_id),
Some(NodeId(2))
);
}
#[test]
fn adjust_return_value_retypes_the_symbol_into_a_literal() {
let mut graph = SyntaxGraph::new();
graph.add_node(
NodeId(1),
SyntaxNode::Return {
value_id: Some(NodeId(2)),
},
);
graph.add_node(NodeId(2), symbol_lookup("x"));
graph.add_node(NodeId(3), variable_declaration("x", NodeId(4)));
graph.add_node(NodeId(4), literal("\"ok\"", "string"));
graph.add_ast_edge(NodeId(1), NodeId(2));
graph.add_cfg_edge(NodeId(3), NodeId(1));
adjust_return_value(&mut graph, NodeId(1));
assert_eq!(
graph.nodes.get(&NodeId(2)),
Some(&SyntaxNode::Literal {
value: "ok".to_owned(),
value_type: String::new(),
})
);
}
}