use super::helpers::{
extract_simple_var, extract_string_from_expr, infer_arithmetic, property_assign_compatible,
widen_array_with_value,
};
use super::ExpressionAnalyzer;
use crate::context::Context;
use mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Union};
use php_ast::ast::{AssignExpr, AssignOp, ExprKind};
use php_ast::Span;
impl<'a> ExpressionAnalyzer<'a> {
pub(super) fn analyze_assign<'arena, 'src>(
&mut self,
a: &AssignExpr<'arena, 'src>,
expr_span: Span,
ctx: &mut Context,
) -> Union {
let rhs_tainted = crate::taint::is_expr_tainted(a.value, ctx);
let rhs_ty = self.analyze(a.value, ctx);
if rhs_ty.is_never() {
return rhs_ty;
}
match a.op {
AssignOp::Assign => {
self.assign_to_target(a.target, rhs_ty.clone(), ctx, expr_span);
if rhs_tainted {
if let php_ast::ast::ExprKind::Variable(name) = &a.target.kind {
ctx.taint_var(name.as_ref());
}
}
rhs_ty
}
AssignOp::Concat => {
if let Some(var_name) = extract_simple_var(a.target) {
ctx.set_var(&var_name, Union::single(Atomic::TString));
let (line, col_start) = self.offset_to_line_col(a.target.span.start);
let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
}
Union::single(Atomic::TString)
}
AssignOp::Plus
| AssignOp::Minus
| AssignOp::Mul
| AssignOp::Div
| AssignOp::Mod
| AssignOp::Pow => {
let lhs_ty = self.analyze(a.target, ctx);
let result_ty = infer_arithmetic(&lhs_ty, &rhs_ty);
if let Some(var_name) = extract_simple_var(a.target) {
ctx.set_var(&var_name, result_ty.clone());
let (line, col_start) = self.offset_to_line_col(a.target.span.start);
let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
}
result_ty
}
AssignOp::Coalesce => {
let lhs_ty = self.analyze(a.target, ctx);
let merged = Union::merge(&lhs_ty.remove_null(), &rhs_ty);
if let Some(var_name) = extract_simple_var(a.target) {
ctx.set_var(&var_name, merged.clone());
let (line, col_start) = self.offset_to_line_col(a.target.span.start);
let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
}
merged
}
_ => {
if let Some(var_name) = extract_simple_var(a.target) {
ctx.set_var(&var_name, Union::mixed());
let (line, col_start) = self.offset_to_line_col(a.target.span.start);
let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
}
Union::mixed()
}
}
}
pub(super) fn assign_to_target<'arena, 'src>(
&mut self,
target: &php_ast::ast::Expr<'arena, 'src>,
ty: Union,
ctx: &mut Context,
span: Span,
) {
match &target.kind {
ExprKind::Variable(name) => {
let name_str = name.as_str().trim_start_matches('$').to_string();
if ctx.byref_param_names.contains(&name_str) {
ctx.read_vars.insert(name_str.clone());
}
ctx.set_var(name_str.clone(), ty);
let (line, col_start) = self.offset_to_line_col(target.span.start);
let (line_end, col_end) = self.offset_to_line_col(target.span.end);
ctx.record_var_location(&name_str, line, col_start, line_end, col_end);
}
ExprKind::Array(elements) => {
let has_non_array = ty.contains(|a| matches!(a, Atomic::TFalse | Atomic::TNull));
let has_array = ty.contains(|a| {
matches!(
a,
Atomic::TArray { .. }
| Atomic::TList { .. }
| Atomic::TNonEmptyArray { .. }
| Atomic::TNonEmptyList { .. }
| Atomic::TKeyedArray { .. }
)
});
if has_non_array && has_array {
self.emit(
IssueKind::PossiblyInvalidArrayOffset {
expected: "array".to_string(),
actual: format!("{ty}"),
},
Severity::Warning,
span,
);
}
let value_ty: Union = ty
.types
.iter()
.find_map(|a| match a {
Atomic::TArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TNonEmptyList { value } => Some(*value.clone()),
_ => None,
})
.unwrap_or_else(Union::mixed);
for elem in elements.iter() {
self.assign_to_target(&elem.value, value_ty.clone(), ctx, span);
}
}
ExprKind::PropertyAccess(pa) => {
let obj_ty = self.analyze(pa.object, ctx);
if let Some(prop_name) = extract_string_from_expr(pa.property) {
for atomic in &obj_ty.types {
if let Atomic::TNamedObject { fqcn, .. } = atomic {
let db = self.db;
let prop_info: Option<(bool, Option<Union>)> = db
.lookup_property_node(fqcn, &prop_name)
.filter(|n| n.active(db))
.map(|n| (n.is_readonly(db), n.ty(db)));
if let Some((is_readonly, prop_ty)) = prop_info {
if is_readonly && !ctx.inside_constructor {
self.emit(
IssueKind::ReadonlyPropertyAssignment {
class: fqcn.to_string(),
property: prop_name.clone(),
},
Severity::Error,
span,
);
}
if let Some(prop_ty) = &prop_ty {
if !prop_ty.is_mixed()
&& !ty.is_mixed()
&& !property_assign_compatible(&ty, prop_ty, self.db)
{
self.emit(
IssueKind::InvalidPropertyAssignment {
property: prop_name.clone(),
expected: format!("{prop_ty}"),
actual: format!("{ty}"),
},
Severity::Warning,
span,
);
}
}
}
}
}
}
}
ExprKind::StaticPropertyAccess(_) => {}
ExprKind::ArrayAccess(aa) => {
if let Some(idx) = &aa.index {
self.analyze(idx, ctx);
}
let mut base = aa.array;
loop {
match &base.kind {
ExprKind::Variable(name) => {
let name_str = name.as_str().trim_start_matches('$');
if !ctx.var_is_defined(name_str) {
ctx.vars.insert(
name_str.to_string(),
Union::single(Atomic::TArray {
key: Box::new(Union::mixed()),
value: Box::new(ty.clone()),
}),
);
ctx.assigned_vars.insert(name_str.to_string());
} else {
let current = ctx.get_var(name_str);
let updated = widen_array_with_value(¤t, &ty);
ctx.set_var(name_str, updated);
}
break;
}
ExprKind::ArrayAccess(inner) => {
if let Some(idx) = &inner.index {
self.analyze(idx, ctx);
}
base = inner.array;
}
_ => break,
}
}
}
ExprKind::VariableVariable(inner) => {
if let Some(var_name) = extract_simple_var(inner) {
ctx.read_vars.insert(var_name.clone());
let var_ty = ctx.get_var(&var_name);
for atomic in &var_ty.types {
if let Atomic::TLiteralString(accessed_var_name) = atomic {
ctx.set_var(accessed_var_name.to_string(), ty.clone());
let (line, col_start) = self.offset_to_line_col(target.span.start);
let (line_end, col_end) = self.offset_to_line_col(target.span.end);
ctx.record_var_location(
accessed_var_name,
line,
col_start,
line_end,
col_end,
);
}
}
}
}
_ => {}
}
}
}