use std::collections::HashSet;
use std::ops::ControlFlow;
use std::sync::Arc;
use mir_codebase::definitions::{MemberMap, MethodDef};
use php_ast::ast::{UnaryPostfixOp, UnaryPrefixOp};
use php_ast::owned::visitor::OwnedVisitor;
use php_ast::owned::{ClassMember, ClassMemberKind, Expr, ExprKind, MethodDecl, Stmt, StmtKind};
const PURE_BUILTINS: &[&str] = &[
"abs",
"array_key_exists",
"array_keys",
"array_merge",
"array_values",
"count",
"get_class",
"implode",
"in_array",
"intdiv",
"is_array",
"is_bool",
"is_int",
"is_null",
"is_numeric",
"is_object",
"is_string",
"max",
"min",
"sizeof",
"sprintf",
"str_contains",
"str_ends_with",
"str_starts_with",
"strlen",
"strtolower",
"strtoupper",
"substr",
"trim",
];
pub(super) fn infer_mutation_free(
members: &[ClassMember],
own_methods: &mut MemberMap<Arc<MethodDef>>,
) {
let mut candidates: Vec<(String, &MethodDecl)> = members
.iter()
.filter_map(|m| match &m.kind {
ClassMemberKind::Method(m) if has_inferable_signature(m, own_methods) => {
Some((crate::util::php_ident_lowercase(m.name.as_deref()?), m))
}
_ => None,
})
.collect();
loop {
let safe: HashSet<&str> = candidates.iter().map(|(k, _)| k.as_str()).collect();
let before = candidates.len();
let keep: Vec<bool> = candidates
.iter()
.map(|(_, m)| body_writes_no_properties(m, &safe))
.collect();
let mut keep = keep.into_iter();
candidates.retain(|_| keep.next().unwrap_or(false));
if candidates.len() == before {
break;
}
}
for (key, _) in &candidates {
if let Some(def) = own_methods.get_mut(key.as_str()) {
Arc::make_mut(def).inferred_mutation_free = true;
}
}
}
fn has_inferable_signature(m: &MethodDecl, own_methods: &MemberMap<Arc<MethodDef>>) -> bool {
let Some(name) = m.name.as_deref() else {
return false;
};
m.body.is_some()
&& !m.by_ref
&& m.params.iter().all(|p| !p.by_ref)
&& own_methods
.get(crate::util::php_ident_lowercase(name).as_str())
.is_some_and(|d| !d.is_impure && d.self_out.is_none())
}
fn body_writes_no_properties(m: &MethodDecl, safe: &HashSet<&str>) -> bool {
let Some(body) = &m.body else {
return false;
};
let mut scan = BodyScan { safe, clean: true };
body.stmts
.iter()
.all(|s| scan.visit_stmt(s).is_continue() && scan.clean)
}
struct BodyScan<'a> {
safe: &'a HashSet<&'a str>,
clean: bool,
}
impl BodyScan<'_> {
fn reject(&mut self) -> ControlFlow<()> {
self.clean = false;
ControlFlow::Break(())
}
}
fn is_local_target(e: &Expr) -> bool {
match &e.kind {
ExprKind::Variable(name) => name.trim_start_matches('$') != "this",
ExprKind::ArrayAccess(a) => is_local_target(&a.array),
ExprKind::Parenthesized(inner) => is_local_target(inner),
ExprKind::Array(elements) => elements
.iter()
.all(|el| !el.by_ref && !el.unpack && is_local_target(&el.value)),
ExprKind::Omit => true,
_ => false,
}
}
fn is_property_rooted(e: &Expr) -> bool {
match &e.kind {
ExprKind::PropertyAccess(_)
| ExprKind::NullsafePropertyAccess(_)
| ExprKind::StaticPropertyAccess(_)
| ExprKind::StaticPropertyAccessDynamic { .. } => true,
ExprKind::ArrayAccess(a) => is_property_rooted(&a.array),
ExprKind::Parenthesized(inner) => is_property_rooted(inner),
_ => false,
}
}
fn is_pure_builtin(name: &str) -> bool {
PURE_BUILTINS.contains(&name.trim_start_matches('\\').to_ascii_lowercase().as_str())
}
fn is_self_reference(e: &Expr) -> bool {
matches!(&e.kind, ExprKind::Identifier(n)
if n.eq_ignore_ascii_case("self") || n.eq_ignore_ascii_case("static"))
}
impl OwnedVisitor for BodyScan<'_> {
fn visit_stmt(&mut self, stmt: &Stmt) -> ControlFlow<()> {
match &stmt.kind {
StmtKind::Unset(targets) if !targets.iter().all(is_local_target) => self.reject(),
StmtKind::Foreach(fe)
if !is_local_target(&fe.value)
|| fe.key.as_ref().is_some_and(|k| !is_local_target(k)) =>
{
self.reject()
}
StmtKind::Global(_)
| StmtKind::StaticVar(_)
| StmtKind::Function(_)
| StmtKind::Class(_)
| StmtKind::Interface(_)
| StmtKind::Trait(_)
| StmtKind::Enum(_) => self.reject(),
_ => crate::recursion::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
let allowed = match &expr.kind {
ExprKind::Assign(a) => !a.by_ref && is_local_target(&a.target),
ExprKind::UnaryPrefix(u) => {
!matches!(
u.op,
UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement
) || is_local_target(&u.operand)
}
ExprKind::UnaryPostfix(u) => {
!matches!(
u.op,
UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement
) || is_local_target(&u.operand)
}
ExprKind::FunctionCall(c) => {
matches!(&c.name.kind, ExprKind::Identifier(n) if is_pure_builtin(n))
&& c.args.iter().all(|a| {
!a.unpack
&& !a.by_ref
&& a.value.as_ref().is_none_or(|v| !is_property_rooted(v))
})
}
ExprKind::MethodCall(c) => {
matches!(&c.object.kind, ExprKind::Variable(v) if v.trim_start_matches('$') == "this")
&& matches!(&c.method.kind, ExprKind::Identifier(n)
if self.safe.contains(crate::util::php_ident_lowercase(n).as_str()))
}
ExprKind::StaticMethodCall(c) => {
is_self_reference(&c.class)
&& matches!(&c.method.kind, ExprKind::Identifier(n)
if self.safe.contains(crate::util::php_ident_lowercase(n).as_str()))
}
ExprKind::NullsafeMethodCall(_)
| ExprKind::StaticDynMethodCall(_)
| ExprKind::New(_)
| ExprKind::Clone(_)
| ExprKind::CloneWith(..)
| ExprKind::Include(..)
| ExprKind::Eval(_)
| ExprKind::ShellExec(_)
| ExprKind::VariableVariable(_)
| ExprKind::Closure(_)
| ExprKind::ArrowFunction(_)
| ExprKind::AnonymousClass(_)
| ExprKind::CallableCreate(_) => false,
_ => true,
};
if !allowed {
return self.reject();
}
crate::recursion::walk_expr(self, expr)
}
}