use crate::flow::cfg::{BlockId, CFG, Terminator};
use crate::flow::dataflow::{DataflowResult, Direction, TransferFunction, find_node_by_id};
use crate::flow::type_inference::InferredType;
use crate::semantics::LanguageSemantics;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PathCondition {
LengthConstraint {
variable: String,
op: ComparisonOp,
value: i64,
},
TypeGuard {
variable: String,
guarded_type: GuardedType,
is_positive: bool,
},
NullCheck {
variable: String,
is_null: bool,
},
UndefinedCheck {
variable: String,
is_undefined: bool,
},
Truthy { variable: String, is_truthy: bool },
InstanceOf {
variable: String,
type_name: String,
is_positive: bool,
},
NumericComparison {
variable: String,
op: ComparisonOp,
value: i64,
},
StringEquality {
variable: String,
value: String,
is_equal: bool,
},
PropertyExists {
object: String,
property: String,
exists: bool,
},
ArrayIncludes {
array: String,
value: String,
includes: bool,
},
Not(Box<PathCondition>),
And(Box<PathCondition>, Box<PathCondition>),
Or(Box<PathCondition>, Box<PathCondition>),
}
impl PathCondition {
pub fn constrained_variables(&self) -> Vec<&str> {
match self {
PathCondition::LengthConstraint { variable, .. } => vec![variable.as_str()],
PathCondition::TypeGuard { variable, .. } => vec![variable.as_str()],
PathCondition::NullCheck { variable, .. } => vec![variable.as_str()],
PathCondition::UndefinedCheck { variable, .. } => vec![variable.as_str()],
PathCondition::Truthy { variable, .. } => vec![variable.as_str()],
PathCondition::InstanceOf { variable, .. } => vec![variable.as_str()],
PathCondition::NumericComparison { variable, .. } => vec![variable.as_str()],
PathCondition::StringEquality { variable, .. } => vec![variable.as_str()],
PathCondition::PropertyExists { object, .. } => vec![object.as_str()],
PathCondition::ArrayIncludes { array, value, .. } => {
vec![array.as_str(), value.as_str()]
}
PathCondition::Not(inner) => inner.constrained_variables(),
PathCondition::And(left, right) => {
let mut vars = left.constrained_variables();
vars.extend(right.constrained_variables());
vars
}
PathCondition::Or(left, right) => {
let mut vars = left.constrained_variables();
vars.extend(right.constrained_variables());
vars
}
}
}
pub fn negate(self) -> PathCondition {
match self {
PathCondition::NullCheck { variable, is_null } => PathCondition::NullCheck {
variable,
is_null: !is_null,
},
PathCondition::UndefinedCheck {
variable,
is_undefined,
} => PathCondition::UndefinedCheck {
variable,
is_undefined: !is_undefined,
},
PathCondition::Truthy {
variable,
is_truthy,
} => PathCondition::Truthy {
variable,
is_truthy: !is_truthy,
},
PathCondition::TypeGuard {
variable,
guarded_type,
is_positive,
} => PathCondition::TypeGuard {
variable,
guarded_type,
is_positive: !is_positive,
},
PathCondition::InstanceOf {
variable,
type_name,
is_positive,
} => PathCondition::InstanceOf {
variable,
type_name,
is_positive: !is_positive,
},
PathCondition::StringEquality {
variable,
value,
is_equal,
} => PathCondition::StringEquality {
variable,
value,
is_equal: !is_equal,
},
PathCondition::PropertyExists {
object,
property,
exists,
} => PathCondition::PropertyExists {
object,
property,
exists: !exists,
},
PathCondition::ArrayIncludes {
array,
value,
includes,
} => PathCondition::ArrayIncludes {
array,
value,
includes: !includes,
},
PathCondition::LengthConstraint {
variable,
op,
value,
} => PathCondition::LengthConstraint {
variable,
op: op.negate(),
value,
},
PathCondition::NumericComparison {
variable,
op,
value,
} => PathCondition::NumericComparison {
variable,
op: op.negate(),
value,
},
PathCondition::Not(inner) => *inner,
PathCondition::And(left, right) => {
PathCondition::Or(Box::new(left.negate()), Box::new(right.negate()))
}
PathCondition::Or(left, right) => {
PathCondition::And(Box::new(left.negate()), Box::new(right.negate()))
}
}
}
pub fn implies(&self, other: &PathCondition) -> bool {
if self == other {
return true;
}
match (self, other) {
(
PathCondition::NullCheck {
variable: v1,
is_null: false,
},
PathCondition::Truthy {
variable: v2,
is_truthy: true,
},
) if v1 == v2 => {
true
}
(
PathCondition::NumericComparison {
variable: v1,
op: ComparisonOp::Lt,
value: val1,
},
PathCondition::NumericComparison {
variable: v2,
op: ComparisonOp::Le,
value: val2,
},
) if v1 == v2 => *val1 <= *val2,
(
PathCondition::NumericComparison {
variable: v1,
op: ComparisonOp::Gt,
value: val1,
},
PathCondition::NumericComparison {
variable: v2,
op: ComparisonOp::Ge,
value: val2,
},
) if v1 == v2 => *val1 >= *val2,
(
PathCondition::LengthConstraint {
variable: v1,
op: ComparisonOp::Lt,
value: val1,
},
PathCondition::LengthConstraint {
variable: v2,
op: ComparisonOp::Le,
value: val2,
},
) if v1 == v2 => *val1 <= *val2,
_ => false,
}
}
pub fn contradicts(&self, other: &PathCondition) -> bool {
match (self, other) {
(
PathCondition::NullCheck {
variable: v1,
is_null: n1,
},
PathCondition::NullCheck {
variable: v2,
is_null: n2,
},
) if v1 == v2 => n1 != n2,
(
PathCondition::TypeGuard {
variable: v1,
guarded_type: t1,
is_positive: true,
},
PathCondition::TypeGuard {
variable: v2,
guarded_type: t2,
is_positive: true,
},
) if v1 == v2 => t1 != t2,
(
PathCondition::NumericComparison {
variable: v1,
op: ComparisonOp::Lt,
value: val1,
},
PathCondition::NumericComparison {
variable: v2,
op: ComparisonOp::Ge,
value: val2,
},
) if v1 == v2 => *val1 <= *val2,
(
PathCondition::NumericComparison {
variable: v1,
op: ComparisonOp::Gt,
value: val1,
},
PathCondition::NumericComparison {
variable: v2,
op: ComparisonOp::Le,
value: val2,
},
) if v1 == v2 => *val1 >= *val2,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ComparisonOp {
Lt,
Le,
Gt,
Ge,
Eq,
Ne,
}
impl ComparisonOp {
pub fn negate(self) -> Self {
match self {
ComparisonOp::Lt => ComparisonOp::Ge,
ComparisonOp::Le => ComparisonOp::Gt,
ComparisonOp::Gt => ComparisonOp::Le,
ComparisonOp::Ge => ComparisonOp::Lt,
ComparisonOp::Eq => ComparisonOp::Ne,
ComparisonOp::Ne => ComparisonOp::Eq,
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"<" => Some(ComparisonOp::Lt),
"<=" => Some(ComparisonOp::Le),
">" => Some(ComparisonOp::Gt),
">=" => Some(ComparisonOp::Ge),
"==" | "===" => Some(ComparisonOp::Eq),
"!=" | "!==" => Some(ComparisonOp::Ne),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GuardedType {
String,
Number,
Boolean,
Object,
Function,
Undefined,
Symbol,
BigInt,
Custom(String),
}
impl GuardedType {
pub fn parse(s: &str) -> Option<Self> {
match s.trim_matches(|c| c == '"' || c == '\'') {
"string" => Some(GuardedType::String),
"number" => Some(GuardedType::Number),
"boolean" => Some(GuardedType::Boolean),
"object" => Some(GuardedType::Object),
"function" => Some(GuardedType::Function),
"undefined" => Some(GuardedType::Undefined),
"symbol" => Some(GuardedType::Symbol),
"bigint" => Some(GuardedType::BigInt),
other if !other.is_empty() => Some(GuardedType::Custom(other.to_string())),
_ => None,
}
}
pub fn to_inferred_type(&self) -> InferredType {
match self {
GuardedType::String => InferredType::String,
GuardedType::Number => InferredType::Number,
GuardedType::Boolean => InferredType::Boolean,
GuardedType::Object => InferredType::Object,
GuardedType::Function => InferredType::Function,
GuardedType::Undefined => InferredType::Undefined,
GuardedType::Symbol | GuardedType::BigInt | GuardedType::Custom(_) => {
InferredType::Unknown
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SymbolicState {
conditions: HashSet<PathCondition>,
feasible: Option<bool>,
}
impl SymbolicState {
pub fn new() -> Self {
Self::default()
}
pub fn with_conditions(conditions: HashSet<PathCondition>) -> Self {
Self {
conditions,
feasible: None,
}
}
pub fn add_condition(&mut self, condition: PathCondition) {
self.conditions.insert(condition);
self.feasible = None; }
pub fn remove_condition(&mut self, condition: &PathCondition) {
self.conditions.remove(condition);
self.feasible = None;
}
pub fn conditions(&self) -> &HashSet<PathCondition> {
&self.conditions
}
pub fn is_empty(&self) -> bool {
self.conditions.is_empty()
}
pub fn get_constraints(&self, var_name: &str) -> Vec<&PathCondition> {
self.conditions
.iter()
.filter(|c| c.constrained_variables().contains(&var_name))
.collect()
}
pub fn is_feasible(&self) -> bool {
if let Some(cached) = self.feasible {
return cached;
}
let conditions: Vec<_> = self.conditions.iter().collect();
for i in 0..conditions.len() {
for j in (i + 1)..conditions.len() {
if conditions[i].contradicts(conditions[j]) {
return false;
}
}
}
true
}
pub fn merge(&self, other: &SymbolicState) -> SymbolicState {
let intersection: HashSet<_> = self
.conditions
.intersection(&other.conditions)
.cloned()
.collect();
SymbolicState {
conditions: intersection,
feasible: None,
}
}
pub fn extend(&mut self, other: &SymbolicState) {
self.conditions.extend(other.conditions.iter().cloned());
self.feasible = None;
}
pub fn is_non_null(&self, var_name: &str) -> bool {
self.conditions.iter().any(|c| {
matches!(c,
PathCondition::NullCheck { variable, is_null: false } if variable == var_name
)
})
}
pub fn is_null(&self, var_name: &str) -> bool {
self.conditions.iter().any(|c| {
matches!(c,
PathCondition::NullCheck { variable, is_null: true } if variable == var_name
)
})
}
pub fn is_truthy(&self, var_name: &str) -> bool {
self.conditions.iter().any(|c| {
matches!(c,
PathCondition::Truthy { variable, is_truthy: true } if variable == var_name
)
})
}
pub fn get_type_guard(&self, var_name: &str) -> Option<&GuardedType> {
self.conditions.iter().find_map(|c| match c {
PathCondition::TypeGuard {
variable,
guarded_type,
is_positive: true,
} if variable == var_name => Some(guarded_type),
_ => None,
})
}
pub fn get_length_constraints(&self, var_name: &str) -> Vec<(ComparisonOp, i64)> {
self.conditions
.iter()
.filter_map(|c| match c {
PathCondition::LengthConstraint {
variable,
op,
value,
} if variable == var_name => Some((*op, *value)),
_ => None,
})
.collect()
}
pub fn get_numeric_constraints(&self, var_name: &str) -> Vec<(ComparisonOp, i64)> {
self.conditions
.iter()
.filter_map(|c| match c {
PathCondition::NumericComparison {
variable,
op,
value,
} if variable == var_name => Some((*op, *value)),
_ => None,
})
.collect()
}
}
#[derive(Debug, Default)]
pub struct SymbolicAnalysisResult {
pub block_entry: HashMap<BlockId, SymbolicState>,
pub block_exit: HashMap<BlockId, SymbolicState>,
pub infeasible_blocks: HashSet<BlockId>,
}
impl SymbolicAnalysisResult {
pub fn state_at_entry(&self, block_id: BlockId) -> Option<&SymbolicState> {
self.block_entry.get(&block_id)
}
pub fn state_at_exit(&self, block_id: BlockId) -> Option<&SymbolicState> {
self.block_exit.get(&block_id)
}
pub fn get_constraints(&self, block_id: BlockId, var_name: &str) -> Vec<&PathCondition> {
self.block_entry
.get(&block_id)
.map(|state| state.get_constraints(var_name))
.unwrap_or_default()
}
pub fn is_infeasible(&self, block_id: BlockId) -> bool {
self.infeasible_blocks.contains(&block_id)
}
pub fn is_non_null_at(&self, block_id: BlockId, var_name: &str) -> bool {
self.block_entry
.get(&block_id)
.map(|state| state.is_non_null(var_name))
.unwrap_or(false)
}
pub fn is_null_at(&self, block_id: BlockId, var_name: &str) -> bool {
self.block_entry
.get(&block_id)
.map(|state| state.is_null(var_name))
.unwrap_or(false)
}
pub fn get_type_guard_at(&self, block_id: BlockId, var_name: &str) -> Option<&GuardedType> {
self.block_entry
.get(&block_id)
.and_then(|state| state.get_type_guard(var_name))
}
}
pub struct ConditionExtractor<'a> {
semantics: &'static LanguageSemantics,
source: &'a [u8],
}
impl<'a> ConditionExtractor<'a> {
pub fn new(semantics: &'static LanguageSemantics, source: &'a [u8]) -> Self {
Self { semantics, source }
}
pub fn extract_condition(&self, node: tree_sitter::Node<'a>) -> Option<PathCondition> {
let kind = node.kind();
if kind == "unary_expression" {
return self.extract_unary_condition(node);
}
if self.semantics.is_binary_expression(kind) || kind == "binary_expression" {
return self.extract_binary_condition(node);
}
if kind == "typeof_expression" || kind == "typeof" {
return None;
}
if kind == "instanceof_expression" {
return self.extract_instanceof_condition(node);
}
if self.semantics.is_member_access(kind) {
let text = node.utf8_text(self.source).ok()?;
return Some(PathCondition::Truthy {
variable: text.to_string(),
is_truthy: true,
});
}
if self.semantics.is_identifier(kind) || kind == "identifier" {
let var_name = node.utf8_text(self.source).ok()?;
return Some(PathCondition::Truthy {
variable: var_name.to_string(),
is_truthy: true,
});
}
if self.semantics.is_call(kind) {
return self.extract_call_condition(node);
}
None
}
fn extract_unary_condition(&self, node: tree_sitter::Node<'a>) -> Option<PathCondition> {
let operator = node.child_by_field_name("operator").or_else(|| {
let mut cursor = node.walk();
node.children(&mut cursor).find(|c| c.kind() == "!")
})?;
let operator_text = operator.utf8_text(self.source).ok()?;
if operator_text == "!" {
let operand = node
.child_by_field_name("argument")
.or_else(|| node.named_child(0))?;
if let Some(inner) = self.extract_condition(operand) {
return Some(inner.negate());
}
let operand_text = operand.utf8_text(self.source).ok()?;
return Some(PathCondition::Truthy {
variable: operand_text.to_string(),
is_truthy: false,
});
}
None
}
fn extract_binary_condition(&self, node: tree_sitter::Node<'a>) -> Option<PathCondition> {
let left = node.child_by_field_name(self.semantics.left_field)?;
let right = node.child_by_field_name(self.semantics.right_field)?;
let operator = node
.child_by_field_name(self.semantics.operator_field)
.or_else(|| {
let mut cursor = node.walk();
node.children(&mut cursor).find(|c| !c.is_named())
})?;
let op_text = operator.utf8_text(self.source).ok()?;
if op_text == "&&" {
let left_cond = self.extract_condition(left)?;
let right_cond = self.extract_condition(right)?;
return Some(PathCondition::And(
Box::new(left_cond),
Box::new(right_cond),
));
}
if op_text == "||" {
let left_cond = self.extract_condition(left)?;
let right_cond = self.extract_condition(right)?;
return Some(PathCondition::Or(Box::new(left_cond), Box::new(right_cond)));
}
if left.kind() == "typeof_expression" || left.kind() == "typeof" {
return self.extract_typeof_comparison(left, right, op_text);
}
if right.kind() == "typeof_expression" || right.kind() == "typeof" {
return self.extract_typeof_comparison(right, left, op_text);
}
if self.is_null_literal(right) {
return self.extract_null_check(left, op_text, true);
}
if self.is_null_literal(left) {
return self.extract_null_check(right, op_text, true);
}
if self.is_undefined_literal(right) {
return self.extract_null_check(left, op_text, false);
}
if self.is_undefined_literal(left) {
return self.extract_null_check(right, op_text, false);
}
if let Some(cond) = self.try_extract_length_check(left, right, op_text) {
return Some(cond);
}
if let Some(cond) = self.try_extract_length_check(right, left, self.flip_operator(op_text))
{
return Some(cond);
}
if let Some(cond) = self.try_extract_numeric_comparison(left, right, op_text) {
return Some(cond);
}
if let Some(cond) = self.try_extract_string_equality(left, right, op_text) {
return Some(cond);
}
if op_text == "in" {
return self.extract_in_check(left, right);
}
None
}
fn extract_typeof_comparison(
&self,
typeof_node: tree_sitter::Node<'a>,
type_literal: tree_sitter::Node<'a>,
op: &str,
) -> Option<PathCondition> {
let variable = typeof_node.named_child(0)?;
let var_name = variable.utf8_text(self.source).ok()?.to_string();
let type_str = type_literal.utf8_text(self.source).ok()?;
let guarded_type = GuardedType::parse(type_str)?;
let is_positive = matches!(op, "==" | "===");
Some(PathCondition::TypeGuard {
variable: var_name,
guarded_type,
is_positive,
})
}
fn extract_null_check(
&self,
var_node: tree_sitter::Node<'a>,
op: &str,
is_null_literal: bool,
) -> Option<PathCondition> {
let var_name = var_node.utf8_text(self.source).ok()?.to_string();
let is_equality = matches!(op, "==" | "===");
if is_null_literal {
Some(PathCondition::NullCheck {
variable: var_name,
is_null: is_equality,
})
} else {
Some(PathCondition::UndefinedCheck {
variable: var_name,
is_undefined: is_equality,
})
}
}
fn try_extract_length_check(
&self,
potential_length: tree_sitter::Node<'a>,
value_node: tree_sitter::Node<'a>,
op: &str,
) -> Option<PathCondition> {
if !self.semantics.is_member_access(potential_length.kind()) {
return None;
}
let property = potential_length.child_by_field_name(self.semantics.property_field)?;
let property_name = property.utf8_text(self.source).ok()?;
if property_name != "length" {
return None;
}
let object = potential_length.child_by_field_name(self.semantics.object_field)?;
let var_name = object.utf8_text(self.source).ok()?.to_string();
let value_text = value_node.utf8_text(self.source).ok()?;
let value: i64 = value_text.parse().ok()?;
let comparison_op = ComparisonOp::parse(op)?;
Some(PathCondition::LengthConstraint {
variable: var_name,
op: comparison_op,
value,
})
}
fn try_extract_numeric_comparison(
&self,
left: tree_sitter::Node<'a>,
right: tree_sitter::Node<'a>,
op: &str,
) -> Option<PathCondition> {
let comparison_op = ComparisonOp::parse(op)?;
if (self.semantics.is_identifier(left.kind()) || left.kind() == "identifier")
&& self.semantics.is_numeric_literal(right.kind())
{
let var_name = left.utf8_text(self.source).ok()?.to_string();
let value_text = right.utf8_text(self.source).ok()?;
let value: i64 = value_text.parse().ok()?;
return Some(PathCondition::NumericComparison {
variable: var_name,
op: comparison_op,
value,
});
}
if self.semantics.is_numeric_literal(left.kind())
&& (self.semantics.is_identifier(right.kind()) || right.kind() == "identifier")
{
let var_name = right.utf8_text(self.source).ok()?.to_string();
let value_text = left.utf8_text(self.source).ok()?;
let value: i64 = value_text.parse().ok()?;
return Some(PathCondition::NumericComparison {
variable: var_name,
op: comparison_op.negate(), value,
});
}
None
}
fn try_extract_string_equality(
&self,
left: tree_sitter::Node<'a>,
right: tree_sitter::Node<'a>,
op: &str,
) -> Option<PathCondition> {
if !matches!(op, "==" | "===" | "!=" | "!==") {
return None;
}
let is_equal = matches!(op, "==" | "===");
if (self.semantics.is_identifier(left.kind()) || left.kind() == "identifier")
&& self.semantics.is_string_literal(right.kind())
{
let var_name = left.utf8_text(self.source).ok()?.to_string();
let value = right.utf8_text(self.source).ok()?;
let value = value.trim_matches(|c| c == '"' || c == '\'').to_string();
return Some(PathCondition::StringEquality {
variable: var_name,
value,
is_equal,
});
}
if self.semantics.is_string_literal(left.kind())
&& (self.semantics.is_identifier(right.kind()) || right.kind() == "identifier")
{
let var_name = right.utf8_text(self.source).ok()?.to_string();
let value = left.utf8_text(self.source).ok()?;
let value = value.trim_matches(|c| c == '"' || c == '\'').to_string();
return Some(PathCondition::StringEquality {
variable: var_name,
value,
is_equal,
});
}
None
}
fn extract_in_check(
&self,
left: tree_sitter::Node<'a>,
right: tree_sitter::Node<'a>,
) -> Option<PathCondition> {
let property = left.utf8_text(self.source).ok()?;
let property = property.trim_matches(|c| c == '"' || c == '\'').to_string();
let object = right.utf8_text(self.source).ok()?.to_string();
Some(PathCondition::PropertyExists {
object,
property,
exists: true,
})
}
fn extract_instanceof_condition(&self, node: tree_sitter::Node<'a>) -> Option<PathCondition> {
let left = node
.child_by_field_name("left")
.or_else(|| node.named_child(0))?;
let right = node
.child_by_field_name("right")
.or_else(|| node.named_child(1))?;
let variable = left.utf8_text(self.source).ok()?.to_string();
let type_name = right.utf8_text(self.source).ok()?.to_string();
Some(PathCondition::InstanceOf {
variable,
type_name,
is_positive: true,
})
}
fn extract_call_condition(&self, node: tree_sitter::Node<'a>) -> Option<PathCondition> {
let func = node
.child_by_field_name(self.semantics.function_field)
.or_else(|| node.named_child(0))?;
let func_text = func.utf8_text(self.source).ok()?;
if func_text == "Array.isArray" {
let args = node.child_by_field_name(self.semantics.arguments_field)?;
let first_arg = args.named_child(0)?;
let var_name = first_arg.utf8_text(self.source).ok()?.to_string();
return Some(PathCondition::InstanceOf {
variable: var_name,
type_name: "Array".to_string(),
is_positive: true,
});
}
if func_text.ends_with(".hasOwnProperty") {
let object = func_text.trim_end_matches(".hasOwnProperty").to_string();
let args = node.child_by_field_name(self.semantics.arguments_field)?;
let first_arg = args.named_child(0)?;
let property = first_arg.utf8_text(self.source).ok()?;
let property = property.trim_matches(|c| c == '"' || c == '\'').to_string();
return Some(PathCondition::PropertyExists {
object,
property,
exists: true,
});
}
if func_text.ends_with(".includes") {
let array = func_text.trim_end_matches(".includes").to_string();
let args = node.child_by_field_name(self.semantics.arguments_field)?;
let first_arg = args.named_child(0)?;
let value = first_arg.utf8_text(self.source).ok()?.to_string();
return Some(PathCondition::ArrayIncludes {
array,
value,
includes: true,
});
}
None
}
fn is_null_literal(&self, node: tree_sitter::Node<'a>) -> bool {
let kind = node.kind();
if self.semantics.is_null_literal(kind) || kind == "null" || kind == "nil" {
return true;
}
if kind == "identifier"
&& let Ok(text) = node.utf8_text(self.source)
{
return text == "null" || text == "nil" || text == "None";
}
false
}
fn is_undefined_literal(&self, node: tree_sitter::Node<'a>) -> bool {
if node.kind() == "undefined" {
return true;
}
if node.kind() == "identifier"
&& let Ok(text) = node.utf8_text(self.source)
{
return text == "undefined";
}
false
}
fn flip_operator<'b>(&self, op: &'b str) -> &'b str {
match op {
"<" => ">",
">" => "<",
"<=" => ">=",
">=" => "<=",
"==" => "==",
"===" => "===",
"!=" => "!=",
"!==" => "!==",
other => other,
}
}
}
pub fn analyze_symbolic_conditions(
cfg: &CFG,
tree: &tree_sitter::Tree,
source: &[u8],
semantics: &'static LanguageSemantics,
) -> SymbolicAnalysisResult {
let mut result = SymbolicAnalysisResult::default();
let extractor = ConditionExtractor::new(semantics, source);
for block in &cfg.blocks {
result.block_entry.insert(block.id, SymbolicState::new());
result.block_exit.insert(block.id, SymbolicState::new());
}
let mut worklist: Vec<BlockId> = vec![cfg.entry];
let mut visited: HashSet<BlockId> = HashSet::new();
while let Some(block_id) = worklist.pop() {
if visited.contains(&block_id) {
continue;
}
visited.insert(block_id);
if block_id >= cfg.blocks.len() {
continue;
}
let block = &cfg.blocks[block_id];
if !block.reachable {
continue;
}
let entry_state = if block.predecessors.is_empty() {
SymbolicState::new()
} else {
let mut merged: Option<SymbolicState> = None;
for &pred_id in &block.predecessors {
if let Some(pred_exit) = result.block_exit.get(&pred_id) {
merged = Some(match merged {
None => pred_exit.clone(),
Some(existing) => existing.merge(pred_exit),
});
}
}
merged.unwrap_or_default()
};
if !entry_state.is_feasible() {
result.infeasible_blocks.insert(block_id);
}
result.block_entry.insert(block_id, entry_state.clone());
match &block.terminator {
Terminator::Branch {
condition_node,
true_block,
false_block,
} => {
if let Some(cond_node) = find_node_by_id(tree, *condition_node)
&& let Some(condition) = extractor.extract_condition(cond_node)
{
let mut true_state = entry_state.clone();
true_state.add_condition(condition.clone());
result.block_entry.insert(*true_block, true_state.clone());
result.block_exit.insert(block_id, true_state);
let mut false_state = entry_state;
false_state.add_condition(condition.negate());
result.block_entry.insert(*false_block, false_state);
worklist.push(*true_block);
worklist.push(*false_block);
continue;
}
result.block_exit.insert(block_id, entry_state.clone());
worklist.push(*true_block);
worklist.push(*false_block);
}
Terminator::Loop {
body,
exit,
condition_node,
} => {
if let Some(cond_id) = condition_node
&& let Some(cond_node) = find_node_by_id(tree, *cond_id)
&& let Some(condition) = extractor.extract_condition(cond_node)
{
let mut body_state = entry_state.clone();
body_state.add_condition(condition.clone());
result.block_entry.insert(*body, body_state);
let mut exit_state = entry_state;
exit_state.add_condition(condition.negate());
result.block_entry.insert(*exit, exit_state);
worklist.push(*body);
worklist.push(*exit);
continue;
}
result.block_exit.insert(block_id, entry_state);
worklist.push(*body);
worklist.push(*exit);
}
Terminator::Switch { cases, .. } => {
result.block_exit.insert(block_id, entry_state);
for (_, target) in cases {
worklist.push(*target);
}
}
_ => {
result.block_exit.insert(block_id, entry_state);
for succ in cfg.successors(block_id) {
worklist.push(succ);
}
}
}
}
result
}
pub fn is_feasible(conditions: &HashSet<PathCondition>) -> bool {
let state = SymbolicState::with_conditions(conditions.clone());
state.is_feasible()
}
pub fn get_constraints<'a>(
conditions: &'a HashSet<PathCondition>,
var_name: &str,
) -> Vec<&'a PathCondition> {
conditions
.iter()
.filter(|c| c.constrained_variables().contains(&var_name))
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SymbolicFact {
pub condition: PathCondition,
}
impl SymbolicFact {
pub fn new(condition: PathCondition) -> Self {
Self { condition }
}
}
pub struct SymbolicTransfer {
semantics: &'static LanguageSemantics,
}
impl SymbolicTransfer {
pub fn new(semantics: &'static LanguageSemantics) -> Self {
Self { semantics }
}
}
impl TransferFunction<SymbolicFact> for SymbolicTransfer {
fn transfer(
&self,
block: &crate::flow::cfg::BasicBlock,
input: &HashSet<SymbolicFact>,
_cfg: &CFG,
source: &[u8],
tree: &tree_sitter::Tree,
) -> HashSet<SymbolicFact> {
let mut output = input.clone();
let extractor = ConditionExtractor::new(self.semantics, source);
if let Terminator::Branch { condition_node, .. } = &block.terminator
&& let Some(cond_node) = find_node_by_id(tree, *condition_node)
&& let Some(condition) = extractor.extract_condition(cond_node)
{
output.insert(SymbolicFact::new(condition));
}
output
}
}
pub fn analyze_symbolic_dataflow(
cfg: &CFG,
tree: &tree_sitter::Tree,
source: &[u8],
semantics: &'static LanguageSemantics,
) -> DataflowResult<SymbolicFact> {
let transfer = SymbolicTransfer::new(semantics);
crate::flow::dataflow::solve(cfg, Direction::Forward, &transfer, source, tree)
}
#[cfg(test)]
mod tests {
use super::*;
use rma_common::Language;
use rma_parser::ParserEngine;
use std::path::Path;
fn parse_js(code: &str) -> rma_parser::ParsedFile {
let config = rma_common::RmaConfig::default();
let parser = ParserEngine::new(config);
parser
.parse_file(Path::new("test.js"), code)
.expect("parse failed")
}
#[test]
fn test_null_check_condition() {
let cond = PathCondition::NullCheck {
variable: "x".to_string(),
is_null: true,
};
assert_eq!(cond.constrained_variables(), vec!["x"]);
}
#[test]
fn test_condition_negation() {
let cond = PathCondition::NullCheck {
variable: "x".to_string(),
is_null: true,
};
let negated = cond.negate();
assert!(matches!(
negated,
PathCondition::NullCheck { is_null: false, .. }
));
}
#[test]
fn test_type_guard_condition() {
let cond = PathCondition::TypeGuard {
variable: "x".to_string(),
guarded_type: GuardedType::String,
is_positive: true,
};
assert_eq!(cond.constrained_variables(), vec!["x"]);
}
#[test]
fn test_length_constraint() {
let cond = PathCondition::LengthConstraint {
variable: "input".to_string(),
op: ComparisonOp::Lt,
value: 10,
};
assert_eq!(cond.constrained_variables(), vec!["input"]);
}
#[test]
fn test_condition_contradiction() {
let cond1 = PathCondition::NullCheck {
variable: "x".to_string(),
is_null: true,
};
let cond2 = PathCondition::NullCheck {
variable: "x".to_string(),
is_null: false,
};
assert!(cond1.contradicts(&cond2));
}
#[test]
fn test_comparison_op_negation() {
assert_eq!(ComparisonOp::Lt.negate(), ComparisonOp::Ge);
assert_eq!(ComparisonOp::Gt.negate(), ComparisonOp::Le);
assert_eq!(ComparisonOp::Eq.negate(), ComparisonOp::Ne);
}
#[test]
fn test_symbolic_state_add_condition() {
let mut state = SymbolicState::new();
state.add_condition(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: false,
});
assert!(!state.is_empty());
assert!(state.is_non_null("x"));
}
#[test]
fn test_symbolic_state_feasibility() {
let mut state = SymbolicState::new();
state.add_condition(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: true,
});
state.add_condition(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: false,
});
assert!(!state.is_feasible());
}
#[test]
fn test_symbolic_state_merge() {
let mut state1 = SymbolicState::new();
state1.add_condition(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: false,
});
state1.add_condition(PathCondition::Truthy {
variable: "y".to_string(),
is_truthy: true,
});
let mut state2 = SymbolicState::new();
state2.add_condition(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: false,
});
let merged = state1.merge(&state2);
assert!(merged.is_non_null("x"));
assert!(!merged.is_truthy("y")); }
#[test]
fn test_get_type_guard() {
let mut state = SymbolicState::new();
state.add_condition(PathCondition::TypeGuard {
variable: "x".to_string(),
guarded_type: GuardedType::String,
is_positive: true,
});
let guard = state.get_type_guard("x");
assert!(matches!(guard, Some(GuardedType::String)));
}
#[test]
fn test_get_length_constraints() {
let mut state = SymbolicState::new();
state.add_condition(PathCondition::LengthConstraint {
variable: "input".to_string(),
op: ComparisonOp::Lt,
value: 10,
});
state.add_condition(PathCondition::LengthConstraint {
variable: "input".to_string(),
op: ComparisonOp::Gt,
value: 0,
});
let constraints = state.get_length_constraints("input");
assert_eq!(constraints.len(), 2);
}
#[test]
fn test_extract_null_check() {
let code = "if (x !== null) { console.log(x); }";
let parsed = parse_js(code);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let cfg = CFG::build(&parsed, Language::JavaScript);
let result = analyze_symbolic_conditions(&cfg, &parsed.tree, code.as_bytes(), semantics);
assert!(!result.block_entry.is_empty());
}
#[test]
fn test_extract_typeof_check() {
let code = r#"if (typeof x === "string") { console.log(x); }"#;
let parsed = parse_js(code);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let cfg = CFG::build(&parsed, Language::JavaScript);
let result = analyze_symbolic_conditions(&cfg, &parsed.tree, code.as_bytes(), semantics);
assert!(!result.block_entry.is_empty());
}
#[test]
fn test_extract_length_check() {
let code = "if (input.length < 10) { process(input); }";
let parsed = parse_js(code);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let cfg = CFG::build(&parsed, Language::JavaScript);
let result = analyze_symbolic_conditions(&cfg, &parsed.tree, code.as_bytes(), semantics);
assert!(!result.block_entry.is_empty());
}
#[test]
fn test_extract_numeric_comparison() {
let code = "if (count > 0) { process(count); }";
let parsed = parse_js(code);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let cfg = CFG::build(&parsed, Language::JavaScript);
let result = analyze_symbolic_conditions(&cfg, &parsed.tree, code.as_bytes(), semantics);
assert!(!result.block_entry.is_empty());
}
#[test]
fn test_branching_conditions() {
let code = r#"
function validate(input) {
if (input !== null) {
if (typeof input === 'string') {
if (input.length > 0) {
return input;
}
}
}
return null;
}
"#;
let parsed = parse_js(code);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let cfg = CFG::build(&parsed, Language::JavaScript);
let result = analyze_symbolic_conditions(&cfg, &parsed.tree, code.as_bytes(), semantics);
assert!(
!result.block_entry.is_empty(),
"Should have analyzed at least one block"
);
if cfg.block_count() > 1 {
let has_some_state = result
.block_entry
.values()
.chain(result.block_exit.values())
.any(|state| !state.is_empty() || state.conditions().is_empty());
assert!(has_some_state, "Should have computed states for blocks");
}
}
#[test]
fn test_infeasible_path_detection() {
let code = r#"
if (x === null) {
if (x !== null) {
// This block should be infeasible
console.log("unreachable");
}
}
"#;
let parsed = parse_js(code);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let cfg = CFG::build(&parsed, Language::JavaScript);
let result = analyze_symbolic_conditions(&cfg, &parsed.tree, code.as_bytes(), semantics);
assert!(result.block_entry.len() > 1);
}
#[test]
fn test_guarded_type_from_str() {
assert_eq!(GuardedType::parse("string"), Some(GuardedType::String));
assert_eq!(GuardedType::parse("number"), Some(GuardedType::Number));
assert_eq!(GuardedType::parse("boolean"), Some(GuardedType::Boolean));
assert_eq!(GuardedType::parse("object"), Some(GuardedType::Object));
assert_eq!(GuardedType::parse("function"), Some(GuardedType::Function));
assert_eq!(
GuardedType::parse("undefined"),
Some(GuardedType::Undefined)
);
assert_eq!(GuardedType::parse("symbol"), Some(GuardedType::Symbol));
assert_eq!(GuardedType::parse("bigint"), Some(GuardedType::BigInt));
assert_eq!(GuardedType::parse("'string'"), Some(GuardedType::String));
assert_eq!(GuardedType::parse("\"number\""), Some(GuardedType::Number));
}
#[test]
fn test_guarded_type_to_inferred() {
assert_eq!(GuardedType::String.to_inferred_type(), InferredType::String);
assert_eq!(GuardedType::Number.to_inferred_type(), InferredType::Number);
assert_eq!(
GuardedType::Boolean.to_inferred_type(),
InferredType::Boolean
);
}
#[test]
fn test_is_feasible_function() {
let mut conditions = HashSet::new();
conditions.insert(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: false,
});
assert!(is_feasible(&conditions));
conditions.insert(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: true,
});
assert!(!is_feasible(&conditions));
}
#[test]
fn test_get_constraints_function() {
let mut conditions = HashSet::new();
conditions.insert(PathCondition::NullCheck {
variable: "x".to_string(),
is_null: false,
});
conditions.insert(PathCondition::Truthy {
variable: "y".to_string(),
is_truthy: true,
});
conditions.insert(PathCondition::LengthConstraint {
variable: "x".to_string(),
op: ComparisonOp::Gt,
value: 0,
});
let x_constraints = get_constraints(&conditions, "x");
assert_eq!(x_constraints.len(), 2);
let y_constraints = get_constraints(&conditions, "y");
assert_eq!(y_constraints.len(), 1);
let z_constraints = get_constraints(&conditions, "z");
assert_eq!(z_constraints.len(), 0);
}
}