#[cfg(test)]
mod tests;
use mago_span::HasSpan;
use mago_syntax::ast::*;
pub(crate) enum ByRefCallKind<'a> {
Function(&'a str),
StaticMethod(&'a str, &'a str),
Constructor(&'a str),
}
pub(crate) type ByRefResolver<'a> = &'a dyn Fn(&ByRefCallKind<'_>) -> Option<Vec<usize>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AccessKind {
Read,
Write,
ReadWrite,
}
#[derive(Debug, Clone)]
pub(crate) struct VarAccess {
pub name: String,
pub offset: u32,
pub kind: AccessKind,
}
#[derive(Debug, Clone)]
pub(crate) struct Frame {
pub start: u32,
pub end: u32,
pub kind: FrameKind,
#[allow(dead_code)] pub captures: Vec<(String, bool)>,
pub parameters: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FrameKind {
TopLevel,
Function,
Method,
Closure,
ArrowFunction,
Catch,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ScopeMap {
pub accesses: Vec<VarAccess>,
pub frames: Vec<Frame>,
pub has_this_or_self: bool,
pub has_reference_params: bool,
}
impl ScopeMap {
pub(crate) fn uses_reference_params(&self) -> bool {
self.has_reference_params
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct RangeClassification {
pub parameters: Vec<String>,
pub return_values: Vec<String>,
pub locals: Vec<String>,
pub uses_this: bool,
pub reference_writes: Vec<String>,
}
impl ScopeMap {
pub(crate) fn enclosing_frame(&self, offset: u32) -> Option<&Frame> {
self.frames
.iter()
.rev()
.find(|f| offset >= f.start && offset <= f.end)
}
pub(crate) fn enclosing_frame_for_range(&self, start: u32, end: u32) -> Option<&Frame> {
self.frames
.iter()
.rev()
.find(|f| start >= f.start && end <= f.end)
}
pub(crate) fn accesses_in_frame<'a>(&'a self, name: &str, frame: &Frame) -> Vec<&'a VarAccess> {
self.accesses
.iter()
.filter(|a| a.name == name && a.offset >= frame.start && a.offset <= frame.end)
.filter(|a| {
!self.frames.iter().any(|f| {
f.start > frame.start
&& f.end < frame.end
&& a.offset >= f.start
&& a.offset <= f.end
&& f.kind != FrameKind::Catch
})
})
.collect()
}
pub(crate) fn classify_range(&self, start: u32, end: u32) -> RangeClassification {
let frame = match self.enclosing_frame_for_range(start, end) {
Some(f) => f,
None => return RangeClassification::default(),
};
let mut var_names: Vec<String> = Vec::new();
for access in &self.accesses {
if access.offset >= start
&& access.offset < end
&& !var_names.contains(&access.name)
&& access.name != "$this"
&& access.name != "self"
&& access.name != "static"
&& access.name != "parent"
{
let in_nested = self.frames.iter().any(|f| {
f.start > frame.start
&& f.end < frame.end
&& access.offset >= f.start
&& access.offset <= f.end
&& f.kind != FrameKind::Catch
});
if !in_nested {
var_names.push(access.name.clone());
}
}
}
let mut result = RangeClassification {
uses_this: self.accesses.iter().any(|a| {
a.offset >= start
&& a.offset < end
&& (a.name == "$this"
|| a.name == "self"
|| a.name == "static"
|| a.name == "parent")
}),
..Default::default()
};
for var_name in &var_names {
let frame_accesses = self.accesses_in_frame(var_name, frame);
let has_write_before = frame_accesses.iter().any(|a| {
a.offset < start && matches!(a.kind, AccessKind::Write | AccessKind::ReadWrite)
});
let has_read_inside = frame_accesses.iter().any(|a| {
a.offset >= start
&& a.offset < end
&& matches!(a.kind, AccessKind::Read | AccessKind::ReadWrite)
});
let has_write_inside = frame_accesses.iter().any(|a| {
a.offset >= start
&& a.offset < end
&& matches!(a.kind, AccessKind::Write | AccessKind::ReadWrite)
});
let has_read_after = frame_accesses.iter().any(|a| {
a.offset >= end && matches!(a.kind, AccessKind::Read | AccessKind::ReadWrite)
});
let first_write = frame_accesses
.iter()
.filter(|a| matches!(a.kind, AccessKind::Write | AccessKind::ReadWrite))
.min_by_key(|a| a.offset);
let last_read = frame_accesses
.iter()
.filter(|a| matches!(a.kind, AccessKind::Read | AccessKind::ReadWrite))
.max_by_key(|a| a.offset);
let entirely_inside = first_write.is_some_and(|w| w.offset >= start && w.offset < end)
&& last_read.is_none_or(|r| r.offset < end)
&& !has_write_before
&& !has_read_after;
if entirely_inside {
result.locals.push(var_name.clone());
} else if has_read_inside && has_write_before && !has_write_inside {
result.parameters.push(var_name.clone());
} else if has_read_inside && has_write_before && has_write_inside {
result.parameters.push(var_name.clone());
if has_read_after {
result.return_values.push(var_name.clone());
}
} else if has_write_inside && has_read_after {
let first_write_inside =
first_write.is_some_and(|w| w.offset >= start && w.offset < end);
let needs_param = has_write_before || (has_read_inside && !first_write_inside);
if needs_param && !result.parameters.contains(var_name) {
result.parameters.push(var_name.clone());
}
if !result.return_values.contains(var_name) {
result.return_values.push(var_name.clone());
}
} else if has_read_inside && !has_write_before && !has_write_inside {
result.parameters.push(var_name.clone());
} else if has_write_inside && !has_read_inside && !has_read_after {
result.locals.push(var_name.clone());
}
}
result.parameters.sort();
result.return_values.sort();
result.locals.sort();
result.reference_writes.sort();
result
}
pub(crate) fn variables_in_scope(&self, offset: u32) -> Vec<String> {
let frame = match self.enclosing_frame(offset) {
Some(f) => f,
None => return Vec::new(),
};
let mut names: Vec<String> = Vec::new();
for access in &self.accesses {
if access.offset >= frame.start
&& access.offset <= frame.end
&& !names.contains(&access.name)
&& access.name != "$this"
{
names.push(access.name.clone());
}
}
names.sort();
names
}
pub(crate) fn all_occurrences(&self, name: &str, offset: u32) -> Vec<(u32, AccessKind)> {
let frame = match self.enclosing_frame(offset) {
Some(f) => f,
None => return Vec::new(),
};
self.accesses_in_frame(name, frame)
.into_iter()
.map(|a| (a.offset, a.kind))
.collect()
}
}
struct Collector<'a> {
accesses: Vec<VarAccess>,
frames: Vec<Frame>,
has_this_or_self: bool,
has_reference_params: bool,
frame_stack: Vec<u32>,
by_ref_resolver: Option<ByRefResolver<'a>>,
}
impl<'a> Collector<'a> {
fn new() -> Self {
Self {
accesses: Vec::new(),
frames: Vec::new(),
has_this_or_self: false,
has_reference_params: false,
frame_stack: Vec::new(),
by_ref_resolver: None,
}
}
fn with_resolver(resolver: ByRefResolver<'a>) -> Self {
Self {
accesses: Vec::new(),
frames: Vec::new(),
has_this_or_self: false,
has_reference_params: false,
frame_stack: Vec::new(),
by_ref_resolver: Some(resolver),
}
}
fn push_access(&mut self, name: String, offset: u32, kind: AccessKind) {
self.accesses.push(VarAccess { name, offset, kind });
}
fn push_frame(&mut self, frame: Frame) {
self.frame_stack.push(frame.start);
self.frames.push(frame);
}
fn pop_frame(&mut self) {
self.frame_stack.pop();
}
}
pub(crate) fn collect_scope(
statements: &[Statement<'_>],
body_start: u32,
body_end: u32,
) -> ScopeMap {
collect_scope_with_resolver(statements, body_start, body_end, None)
}
pub(crate) fn collect_scope_with_resolver(
statements: &[Statement<'_>],
body_start: u32,
body_end: u32,
resolver: Option<ByRefResolver<'_>>,
) -> ScopeMap {
let mut collector = match resolver {
Some(r) => Collector::with_resolver(r),
None => Collector::new(),
};
collector.push_frame(Frame {
start: body_start,
end: body_end,
kind: FrameKind::TopLevel,
captures: Vec::new(),
parameters: Vec::new(),
});
for stmt in statements {
walk_statement(stmt, &mut collector);
}
collector.pop_frame();
collector.frames.sort_by_key(|f| f.start);
ScopeMap {
accesses: collector.accesses,
frames: collector.frames,
has_this_or_self: collector.has_this_or_self,
has_reference_params: collector.has_reference_params,
}
}
pub(crate) fn collect_parameters(
params: &FunctionLikeParameterList<'_>,
collector_accesses: &mut Vec<VarAccess>,
collector_has_reference: &mut bool,
) {
for param in params.parameters.iter() {
let name = param.variable.name.to_string();
let offset = param.variable.span().start.offset;
collector_accesses.push(VarAccess {
name,
offset,
kind: AccessKind::Write,
});
if param.ampersand.is_some() {
*collector_has_reference = true;
}
if let Some(ref default) = param.default_value {
let mut tmp = Collector::new();
walk_expression(default.value, &mut tmp);
collector_accesses.extend(tmp.accesses);
}
}
}
pub(crate) fn collect_function_scope<'a>(
params: &FunctionLikeParameterList<'a>,
body: &[Statement<'a>],
body_start: u32,
body_end: u32,
) -> ScopeMap {
collect_function_scope_with_kind(params, body, body_start, body_end, FrameKind::Function)
}
pub(crate) fn collect_function_scope_with_resolver<'a>(
params: &FunctionLikeParameterList<'a>,
body: &[Statement<'a>],
body_start: u32,
body_end: u32,
resolver: Option<ByRefResolver<'_>>,
) -> ScopeMap {
collect_function_scope_with_kind_and_resolver(
params,
body,
body_start,
body_end,
FrameKind::Function,
resolver,
)
}
pub(crate) fn collect_function_scope_with_kind<'a>(
params: &FunctionLikeParameterList<'a>,
body: &[Statement<'a>],
body_start: u32,
body_end: u32,
kind: FrameKind,
) -> ScopeMap {
collect_function_scope_with_kind_and_resolver(params, body, body_start, body_end, kind, None)
}
pub(crate) fn collect_function_scope_with_kind_and_resolver<'a>(
params: &FunctionLikeParameterList<'a>,
body: &[Statement<'a>],
body_start: u32,
body_end: u32,
kind: FrameKind,
resolver: Option<ByRefResolver<'_>>,
) -> ScopeMap {
let mut collector = match resolver {
Some(r) => Collector::with_resolver(r),
None => Collector::new(),
};
let param_names: Vec<String> = params
.parameters
.iter()
.map(|p| p.variable.name.to_string())
.collect();
collector.push_frame(Frame {
start: body_start,
end: body_end,
kind,
captures: Vec::new(),
parameters: param_names,
});
collect_parameters(
params,
&mut collector.accesses,
&mut collector.has_reference_params,
);
for stmt in body {
walk_statement(stmt, &mut collector);
}
collector.pop_frame();
collector.frames.sort_by_key(|f| f.start);
ScopeMap {
accesses: collector.accesses,
frames: collector.frames,
has_this_or_self: collector.has_this_or_self,
has_reference_params: collector.has_reference_params,
}
}
fn walk_statement(stmt: &Statement<'_>, collector: &mut Collector<'_>) {
match stmt {
Statement::Expression(expr_stmt) => {
walk_expression(expr_stmt.expression, collector);
}
Statement::Return(ret) => {
if let Some(val) = ret.value {
walk_expression(val, collector);
}
}
Statement::Echo(echo) => {
for val in echo.values.iter() {
walk_expression(val, collector);
}
}
Statement::If(if_stmt) => {
walk_expression(if_stmt.condition, collector);
match &if_stmt.body {
IfBody::Statement(if_body) => {
walk_if_statement_body(if_body, collector);
}
IfBody::ColonDelimited(body) => {
for s in body.statements.iter() {
walk_statement(s, collector);
}
for clause in body.else_if_clauses.iter() {
walk_expression(clause.condition, collector);
for s in clause.statements.iter() {
walk_statement(s, collector);
}
}
if let Some(ref else_clause) = body.else_clause {
for s in else_clause.statements.iter() {
walk_statement(s, collector);
}
}
}
}
}
Statement::Foreach(foreach) => {
walk_expression(foreach.expression, collector);
if let Some(key_expr) = foreach.target.key() {
walk_expression_as_write(key_expr, collector);
}
walk_expression_as_write(foreach.target.value(), collector);
match &foreach.body {
ForeachBody::Statement(inner) => {
walk_statement(inner, collector);
}
ForeachBody::ColonDelimited(body) => {
for s in body.statements.iter() {
walk_statement(s, collector);
}
}
}
}
Statement::While(while_stmt) => {
walk_expression(while_stmt.condition, collector);
match &while_stmt.body {
WhileBody::Statement(inner) => {
walk_statement(inner, collector);
}
WhileBody::ColonDelimited(body) => {
for s in body.statements.iter() {
walk_statement(s, collector);
}
}
}
}
Statement::DoWhile(dw) => {
walk_statement(dw.statement, collector);
walk_expression(dw.condition, collector);
}
Statement::For(for_stmt) => {
for init in for_stmt.initializations.iter() {
walk_expression(init, collector);
}
for cond in for_stmt.conditions.iter() {
walk_expression(cond, collector);
}
for inc in for_stmt.increments.iter() {
walk_expression(inc, collector);
}
match &for_stmt.body {
ForBody::Statement(inner) => {
walk_statement(inner, collector);
}
ForBody::ColonDelimited(body) => {
for s in body.statements.iter() {
walk_statement(s, collector);
}
}
}
}
Statement::Switch(switch) => {
walk_expression(switch.expression, collector);
for case in switch.body.cases().iter() {
match case {
SwitchCase::Expression(c) => {
walk_expression(c.expression, collector);
for s in c.statements.iter() {
walk_statement(s, collector);
}
}
SwitchCase::Default(c) => {
for s in c.statements.iter() {
walk_statement(s, collector);
}
}
}
}
}
Statement::Try(try_stmt) => {
for s in try_stmt.block.statements.iter() {
walk_statement(s, collector);
}
for catch in try_stmt.catch_clauses.iter() {
let catch_start = catch.block.left_brace.start.offset;
let catch_end = catch.block.right_brace.end.offset;
let catch_params = if let Some(ref var) = catch.variable {
vec![var.name.to_string()]
} else {
Vec::new()
};
collector.push_frame(Frame {
start: catch_start,
end: catch_end,
kind: FrameKind::Catch,
captures: Vec::new(),
parameters: catch_params,
});
if let Some(ref var) = catch.variable {
let name = var.name.to_string();
collector.push_access(name, var.span().start.offset, AccessKind::Write);
}
for s in catch.block.statements.iter() {
walk_statement(s, collector);
}
collector.pop_frame();
}
if let Some(ref finally) = try_stmt.finally_clause {
for s in finally.block.statements.iter() {
walk_statement(s, collector);
}
}
}
Statement::Block(block) => {
for s in block.statements.iter() {
walk_statement(s, collector);
}
}
Statement::Unset(unset) => {
for val in unset.values.iter() {
walk_expression_as_write(val, collector);
}
}
Statement::Global(global) => {
for var in global.variables.iter() {
if let Variable::Direct(dv) = var {
let name = dv.name.to_string();
collector.push_access(name, dv.span().start.offset, AccessKind::Write);
}
}
}
Statement::Static(static_stmt) => {
for item in static_stmt.items.iter() {
let dv = item.variable();
let name = dv.name.to_string();
collector.push_access(name, dv.span().start.offset, AccessKind::Write);
}
}
Statement::Namespace(ns) => {
for s in ns.statements().iter() {
walk_statement(s, collector);
}
}
Statement::Class(_)
| Statement::Interface(_)
| Statement::Trait(_)
| Statement::Enum(_)
| Statement::Function(_) => {}
_ => {}
}
}
fn walk_if_statement_body(if_body: &IfStatementBody<'_>, collector: &mut Collector<'_>) {
walk_statement(if_body.statement, collector);
for clause in if_body.else_if_clauses.iter() {
walk_expression(clause.condition, collector);
walk_statement(clause.statement, collector);
}
if let Some(ref else_clause) = if_body.else_clause {
walk_statement(else_clause.statement, collector);
}
}
fn walk_expression(expr: &Expression<'_>, collector: &mut Collector<'_>) {
match expr {
Expression::Variable(var) => {
walk_variable_read(var, collector);
}
Expression::Assignment(assignment) => {
walk_assignment(assignment, collector);
}
Expression::Call(call) => {
match call {
Call::Function(func_call) => {
walk_expression(func_call.function, collector);
walk_function_call_arguments(func_call, collector);
}
Call::Method(method_call) => {
walk_expression(method_call.object, collector);
walk_arguments(&method_call.argument_list, collector);
}
Call::NullSafeMethod(method_call) => {
walk_expression(method_call.object, collector);
walk_arguments(&method_call.argument_list, collector);
}
Call::StaticMethod(static_call) => {
walk_expression(static_call.class, collector);
walk_static_method_call_arguments(static_call, collector);
}
}
}
Expression::Access(access) => match access {
Access::Property(pa) => {
walk_expression(pa.object, collector);
}
Access::NullSafeProperty(pa) => {
walk_expression(pa.object, collector);
}
Access::StaticProperty(spa) => {
walk_expression(spa.class, collector);
match &spa.property {
Variable::Direct(_) => {}
Variable::Indirect(iv) => walk_expression(iv.expression, collector),
Variable::Nested(nv) => walk_variable_read(nv.variable, collector),
}
}
Access::ClassConstant(cca) => {
walk_expression(cca.class, collector);
}
},
Expression::ArrayAccess(access) => {
walk_expression(access.array, collector);
walk_expression(access.index, collector);
}
Expression::ArrayAppend(append) => {
walk_expression(append.array, collector);
}
Expression::Array(array) => {
for element in array.elements.iter() {
walk_array_element(element, collector);
}
}
Expression::LegacyArray(array) => {
for element in array.elements.iter() {
walk_array_element(element, collector);
}
}
Expression::List(list) => {
for element in list.elements.iter() {
walk_array_element(element, collector);
}
}
Expression::Closure(closure) => {
walk_closure(closure, collector);
}
Expression::ArrowFunction(arrow) => {
walk_arrow_function(arrow, collector);
}
Expression::Parenthesized(paren) => {
walk_expression(paren.expression, collector);
}
Expression::UnaryPrefix(unary) => {
walk_expression(unary.operand, collector);
}
Expression::UnaryPostfix(unary) => {
if let Expression::Variable(Variable::Direct(dv)) = unary.operand {
let name = dv.name.to_string();
collector.push_access(name, dv.span().start.offset, AccessKind::ReadWrite);
} else {
walk_expression(unary.operand, collector);
}
}
Expression::Binary(binary) => {
walk_expression(binary.lhs, collector);
walk_expression(binary.rhs, collector);
}
Expression::Conditional(cond) => {
walk_expression(cond.condition, collector);
if let Some(then_expr) = cond.then {
walk_expression(then_expr, collector);
}
walk_expression(cond.r#else, collector);
}
Expression::Instantiation(inst) => {
walk_expression(inst.class, collector);
if let Some(ref args) = inst.argument_list {
walk_constructor_arguments(inst, args, collector);
}
}
Expression::Throw(throw) => {
walk_expression(throw.exception, collector);
}
Expression::Yield(yield_expr) => match yield_expr {
Yield::Value(yv) => {
if let Some(val) = yv.value {
walk_expression(val, collector);
}
}
Yield::Pair(yp) => {
walk_expression(yp.key, collector);
walk_expression(yp.value, collector);
}
Yield::From(yf) => {
walk_expression(yf.iterator, collector);
}
},
Expression::Clone(clone) => {
walk_expression(clone.object, collector);
}
Expression::Match(match_expr) => {
walk_expression(match_expr.expression, collector);
for arm in match_expr.arms.iter() {
match arm {
MatchArm::Expression(expr_arm) => {
for cond in expr_arm.conditions.iter() {
walk_expression(cond, collector);
}
walk_expression(expr_arm.expression, collector);
}
MatchArm::Default(default_arm) => {
walk_expression(default_arm.expression, collector);
}
}
}
}
Expression::Self_(_) => {
collector.has_this_or_self = true;
collector.push_access(
"self".to_string(),
expr.span().start.offset,
AccessKind::Read,
);
}
Expression::Static(_) => {
collector.has_this_or_self = true;
collector.push_access(
"static".to_string(),
expr.span().start.offset,
AccessKind::Read,
);
}
Expression::Parent(_) => {
collector.has_this_or_self = true;
collector.push_access(
"parent".to_string(),
expr.span().start.offset,
AccessKind::Read,
);
}
Expression::Construct(construct) => match construct {
Construct::Isset(isset) => {
for val in isset.values.iter() {
walk_expression(val, collector);
}
}
Construct::Empty(empty) => {
walk_expression(empty.value, collector);
}
Construct::Eval(eval) => {
walk_expression(eval.value, collector);
}
Construct::Include(inc) => {
walk_expression(inc.value, collector);
}
Construct::IncludeOnce(inc) => {
walk_expression(inc.value, collector);
}
Construct::Require(req) => {
walk_expression(req.value, collector);
}
Construct::RequireOnce(req) => {
walk_expression(req.value, collector);
}
Construct::Print(print) => {
walk_expression(print.value, collector);
}
Construct::Exit(exit) => {
if let Some(ref args) = exit.arguments {
walk_arguments(args, collector);
}
}
Construct::Die(die) => {
if let Some(ref args) = die.arguments {
walk_arguments(args, collector);
}
}
},
Expression::CompositeString(composite) => {
for part in composite.parts().iter() {
match part {
StringPart::Expression(inner_expr) => {
walk_expression(inner_expr, collector);
}
StringPart::BracedExpression(braced) => {
walk_expression(braced.expression, collector);
}
StringPart::Literal(_) => {}
}
}
}
Expression::ConstantAccess(_) => {
}
Expression::Pipe(pipe) => {
walk_expression(pipe.input, collector);
walk_expression(pipe.callable, collector);
}
Expression::PartialApplication(partial) => match partial {
PartialApplication::Function(func_pa) => {
walk_expression(func_pa.function, collector);
}
PartialApplication::Method(method_pa) => {
walk_expression(method_pa.object, collector);
}
PartialApplication::StaticMethod(static_pa) => {
walk_expression(static_pa.class, collector);
}
},
Expression::AnonymousClass(anon) => {
if let Some(ref args) = anon.argument_list {
walk_arguments(args, collector);
}
}
Expression::Literal(_)
| Expression::MagicConstant(_)
| Expression::Identifier(_)
| Expression::Error(_) => {}
_ => {}
}
}
fn walk_expression_as_write(expr: &Expression<'_>, collector: &mut Collector<'_>) {
match expr {
Expression::Variable(Variable::Direct(dv)) => {
let name = dv.name.to_string();
collector.push_access(name, dv.span().start.offset, AccessKind::Write);
}
Expression::Variable(Variable::Indirect(iv)) => {
walk_expression(iv.expression, collector);
}
Expression::Variable(Variable::Nested(nv)) => {
walk_variable_read(nv.variable, collector);
}
Expression::Array(array) => {
for element in array.elements.iter() {
match element {
ArrayElement::KeyValue(kv) => {
walk_expression_as_write(kv.value, collector);
}
ArrayElement::Value(v) => {
walk_expression_as_write(v.value, collector);
}
ArrayElement::Variadic(spread) => {
walk_expression_as_write(spread.value, collector);
}
ArrayElement::Missing(_) => {}
}
}
}
Expression::List(list) => {
for entry in list.elements.iter() {
match entry {
ArrayElement::KeyValue(kv) => {
walk_expression_as_write(kv.value, collector);
}
ArrayElement::Value(v) => {
walk_expression_as_write(v.value, collector);
}
ArrayElement::Variadic(spread) => {
walk_expression_as_write(spread.value, collector);
}
ArrayElement::Missing(_) => {}
}
}
}
Expression::ArrayAccess(access) => {
if let Expression::Variable(Variable::Direct(dv)) = access.array {
let name = dv.name.to_string();
collector.push_access(name, dv.span().start.offset, AccessKind::ReadWrite);
} else {
walk_expression(access.array, collector);
}
walk_expression(access.index, collector);
}
Expression::ArrayAppend(append) => {
if let Expression::Variable(Variable::Direct(dv)) = append.array {
let name = dv.name.to_string();
collector.push_access(name, dv.span().start.offset, AccessKind::ReadWrite);
} else {
walk_expression(append.array, collector);
}
}
Expression::Access(Access::Property(pa)) => {
walk_expression(pa.object, collector);
}
Expression::Access(Access::NullSafeProperty(pa)) => {
walk_expression(pa.object, collector);
}
Expression::Access(Access::StaticProperty(spa)) => {
walk_expression(spa.class, collector);
match &spa.property {
Variable::Direct(_) => {}
Variable::Indirect(iv) => walk_expression(iv.expression, collector),
Variable::Nested(nv) => walk_variable_read(nv.variable, collector),
}
}
_ => {
walk_expression(expr, collector);
}
}
}
fn walk_variable_read(var: &Variable<'_>, collector: &mut Collector<'_>) {
match var {
Variable::Direct(dv) => {
let name = dv.name.to_string();
let offset = dv.span().start.offset;
if name == "$this" {
collector.has_this_or_self = true;
}
collector.push_access(name, offset, AccessKind::Read);
}
Variable::Indirect(iv) => {
walk_expression(iv.expression, collector);
}
Variable::Nested(nv) => {
walk_variable_read(nv.variable, collector);
}
}
}
fn walk_assignment(assignment: &Assignment<'_>, collector: &mut Collector<'_>) {
let is_compound = !assignment.operator.is_assign();
if is_compound {
if let Expression::Variable(Variable::Direct(dv)) = assignment.lhs {
let name = dv.name.to_string();
collector.push_access(name, dv.span().start.offset, AccessKind::ReadWrite);
} else {
walk_expression(assignment.lhs, collector);
}
} else {
walk_expression_as_write(assignment.lhs, collector);
}
walk_expression(assignment.rhs, collector);
}
fn walk_arguments(args: &ArgumentList<'_>, collector: &mut Collector<'_>) {
for arg in args.arguments.iter() {
walk_expression(arg.value(), collector);
}
}
const BY_REF_OUT_PARAMS: &[(&str, &[usize])] = &[
("preg_match", &[2]),
("preg_match_all", &[2]),
("preg_replace", &[4]),
("preg_replace_callback", &[4]),
("exec", &[1, 2]),
("mb_parse_str", &[1]),
("parse_str", &[1]),
("similar_text", &[2]),
("sscanf", &[2, 3, 4, 5, 6, 7]),
("curl_multi_exec", &[1]),
("curl_multi_info_read", &[1]),
("fsockopen", &[2, 3]),
("pfsockopen", &[2, 3]),
("socket_create_pair", &[3]),
("stream_socket_accept", &[2]),
("stream_socket_client", &[1, 2]),
("stream_socket_recvfrom", &[3]),
("stream_socket_server", &[1, 2]),
("openssl_csr_new", &[1]),
("openssl_encrypt", &[6]),
("openssl_open", &[1]),
("openssl_pkey_export", &[1]),
("openssl_pkcs12_export", &[1]),
("openssl_pkcs12_read", &[1]),
("openssl_public_encrypt", &[1]),
("openssl_random_pseudo_bytes", &[1]),
("openssl_seal", &[1, 2, 6]),
("openssl_sign", &[1]),
("pcntl_wait", &[0]),
("pcntl_waitpid", &[1]),
("exif_thumbnail", &[1, 2, 3]),
("getimagesize", &[1]),
("getimagesizefromstring", &[1]),
("dns_get_mx", &[1, 2]),
("dns_get_record", &[2, 3]),
("getmxrr", &[1, 2]),
("flock", &[2]),
("msg_receive", &[2, 4, 7]),
("msg_send", &[5]),
("ldap_parse_result", &[2, 3, 4, 5, 6]),
("getopt", &[2]),
("grapheme_extract", &[4]),
("headers_sent", &[0, 1]),
];
fn walk_function_call_arguments(func_call: &FunctionCall<'_>, collector: &mut Collector<'_>) {
let func_name = match func_call.function {
Expression::Identifier(ident) => {
let raw = ident.value();
raw.strip_prefix('\\').unwrap_or(raw)
}
_ => {
walk_arguments(&func_call.argument_list, collector);
return;
}
};
let hardcoded = BY_REF_OUT_PARAMS
.iter()
.find(|(name, _)| *name == func_name)
.map(|(_, positions)| *positions);
let resolved_positions: Vec<usize>;
let out_positions: Option<&[usize]> = if let Some(positions) = hardcoded {
Some(positions)
} else if let Some(ref resolver) = collector.by_ref_resolver {
let kind = ByRefCallKind::Function(func_name);
if let Some(positions) = resolver(&kind) {
resolved_positions = positions;
if resolved_positions.is_empty() {
None
} else {
Some(&resolved_positions)
}
} else {
None
}
} else {
None
};
match out_positions {
Some(positions) => {
for (idx, arg) in func_call.argument_list.arguments.iter().enumerate() {
if positions.contains(&idx) {
walk_expression_as_write(arg.value(), collector);
} else {
walk_expression(arg.value(), collector);
}
}
}
None => {
walk_arguments(&func_call.argument_list, collector);
}
}
}
fn walk_static_method_call_arguments(
static_call: &StaticMethodCall<'_>,
collector: &mut Collector<'_>,
) {
let class_name = match static_call.class {
Expression::Identifier(ident) => ident.value(),
Expression::Self_(_) => "self",
Expression::Static(_) => "static",
Expression::Parent(_) => "parent",
_ => {
walk_arguments(&static_call.argument_list, collector);
return;
}
};
let method_name = match &static_call.method {
ClassLikeMemberSelector::Identifier(ident) => ident.value,
_ => {
walk_arguments(&static_call.argument_list, collector);
return;
}
};
if let Some(ref resolver) = collector.by_ref_resolver {
let kind = ByRefCallKind::StaticMethod(class_name, method_name);
if let Some(positions) = resolver(&kind)
&& !positions.is_empty()
{
for (idx, arg) in static_call.argument_list.arguments.iter().enumerate() {
if positions.contains(&idx) {
walk_expression_as_write(arg.value(), collector);
} else {
walk_expression(arg.value(), collector);
}
}
return;
}
}
walk_arguments(&static_call.argument_list, collector);
}
fn walk_constructor_arguments(
inst: &Instantiation<'_>,
args: &ArgumentList<'_>,
collector: &mut Collector<'_>,
) {
let class_name = match inst.class {
Expression::Identifier(ident) => ident.value(),
Expression::Self_(_) => "self",
Expression::Static(_) => "static",
Expression::Parent(_) => "parent",
_ => {
walk_arguments(args, collector);
return;
}
};
if let Some(ref resolver) = collector.by_ref_resolver {
let kind = ByRefCallKind::Constructor(class_name);
if let Some(positions) = resolver(&kind)
&& !positions.is_empty()
{
for (idx, arg) in args.arguments.iter().enumerate() {
if positions.contains(&idx) {
walk_expression_as_write(arg.value(), collector);
} else {
walk_expression(arg.value(), collector);
}
}
return;
}
}
walk_arguments(args, collector);
}
fn walk_array_element(element: &ArrayElement<'_>, collector: &mut Collector<'_>) {
match element {
ArrayElement::KeyValue(kv) => {
walk_expression(kv.key, collector);
walk_expression(kv.value, collector);
}
ArrayElement::Value(v) => {
walk_expression(v.value, collector);
}
ArrayElement::Variadic(spread) => {
walk_expression(spread.value, collector);
}
ArrayElement::Missing(_) => {}
}
}
fn walk_closure(closure: &Closure<'_>, collector: &mut Collector<'_>) {
let body_start = closure.body.left_brace.start.offset;
let body_end = closure.body.right_brace.end.offset;
let mut captures = Vec::new();
if let Some(ref use_clause) = closure.use_clause {
for var in use_clause.variables.iter() {
let name = var.variable.name.to_string();
let is_ref = var.ampersand.is_some();
captures.push((name.clone(), is_ref));
collector.push_access(name, var.variable.span().start.offset, AccessKind::Read);
}
}
let param_names: Vec<String> = closure
.parameter_list
.parameters
.iter()
.map(|p| p.variable.name.to_string())
.collect();
collector.push_frame(Frame {
start: body_start,
end: body_end,
kind: FrameKind::Closure,
captures: captures.clone(),
parameters: param_names,
});
for param in closure.parameter_list.parameters.iter() {
let name = param.variable.name.to_string();
let offset = param.variable.span().start.offset;
collector.push_access(name, offset, AccessKind::Write);
if param.ampersand.is_some() {
collector.has_reference_params = true;
}
}
for (cap_name, _is_ref) in &captures {
collector.push_access(cap_name.clone(), body_start, AccessKind::Write);
}
for stmt in closure.body.statements.iter() {
walk_statement(stmt, collector);
}
collector.pop_frame();
}
fn walk_arrow_function(arrow: &ArrowFunction<'_>, collector: &mut Collector<'_>) {
let body_start = arrow.arrow.start.offset;
let body_end = arrow.expression.span().end.offset;
let param_names: Vec<String> = arrow
.parameter_list
.parameters
.iter()
.map(|p| p.variable.name.to_string())
.collect();
collector.push_frame(Frame {
start: body_start,
end: body_end,
kind: FrameKind::ArrowFunction,
captures: Vec::new(), parameters: param_names,
});
for param in arrow.parameter_list.parameters.iter() {
let name = param.variable.name.to_string();
let offset = param.variable.span().start.offset;
collector.push_access(name, offset, AccessKind::Write);
if param.ampersand.is_some() {
collector.has_reference_params = true;
}
}
walk_expression(arrow.expression, collector);
collector.pop_frame();
}