Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions src/diagnostics/undefined_variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,315 @@ fn collect_compact_from_expr(expr: &Expression<'_>, vars: &mut HashSet<String>)
}
}

// ─── get_defined_vars() detection ───────────────────────────────────────────

/// Returns true if the statements contain a call to `get_defined_vars()`.
/// When present in a scope, all variables defined in that scope are
/// considered used (e.g. for debug dumps), so unused-variable diagnostics
/// should be suppressed for them.
pub(crate) fn has_get_defined_vars(statements: &[Statement<'_>]) -> bool {
for stmt in statements {
if stmt_has_get_defined_vars(stmt) {
return true;
}
}
false
}

fn stmt_has_get_defined_vars(stmt: &Statement<'_>) -> bool {
match stmt {
Statement::Expression(es) => expr_has_get_defined_vars(es.expression),
Statement::Return(ret) => ret.value.is_some_and(|v| expr_has_get_defined_vars(v)),
Statement::Echo(echo) => echo.values.iter().any(|v| expr_has_get_defined_vars(v)),
Statement::If(if_stmt) => {
if expr_has_get_defined_vars(if_stmt.condition) {
return true;
}
match &if_stmt.body {
IfBody::Statement(body) => {
if stmt_has_get_defined_vars(body.statement) {
return true;
}
for clause in body.else_if_clauses.iter() {
if expr_has_get_defined_vars(clause.condition)
|| stmt_has_get_defined_vars(clause.statement)
{
return true;
}
}
if let Some(ref el) = body.else_clause
&& stmt_has_get_defined_vars(el.statement)
{
return true;
}
}
IfBody::ColonDelimited(body) => {
for s in body.statements.iter() {
if stmt_has_get_defined_vars(s) {
return true;
}
}
for clause in body.else_if_clauses.iter() {
if expr_has_get_defined_vars(clause.condition) {
return true;
}
for s in clause.statements.iter() {
if stmt_has_get_defined_vars(s) {
return true;
}
}
}
if let Some(ref el) = body.else_clause {
for s in el.statements.iter() {
if stmt_has_get_defined_vars(s) {
return true;
}
}
}
}
}
false
}
Statement::Foreach(foreach) => {
expr_has_get_defined_vars(foreach.expression)
|| match &foreach.body {
ForeachBody::Statement(s) => stmt_has_get_defined_vars(s),
ForeachBody::ColonDelimited(b) => {
b.statements.iter().any(|s| stmt_has_get_defined_vars(s))
}
}
}
Statement::While(w) => {
expr_has_get_defined_vars(w.condition)
|| match &w.body {
WhileBody::Statement(s) => stmt_has_get_defined_vars(s),
WhileBody::ColonDelimited(b) => {
b.statements.iter().any(|s| stmt_has_get_defined_vars(s))
}
}
}
Statement::DoWhile(dw) => {
stmt_has_get_defined_vars(dw.statement) || expr_has_get_defined_vars(dw.condition)
}
Statement::For(for_stmt) => {
for_stmt
.initializations
.iter()
.any(|e| expr_has_get_defined_vars(e))
|| for_stmt
.conditions
.iter()
.any(|e| expr_has_get_defined_vars(e))
|| for_stmt
.increments
.iter()
.any(|e| expr_has_get_defined_vars(e))
|| match &for_stmt.body {
ForBody::Statement(s) => stmt_has_get_defined_vars(s),
ForBody::ColonDelimited(b) => {
b.statements.iter().any(|s| stmt_has_get_defined_vars(s))
}
}
}
Statement::Switch(sw) => {
expr_has_get_defined_vars(sw.expression)
|| sw.body.cases().iter().any(|c| match c {
SwitchCase::Expression(sc) => {
expr_has_get_defined_vars(sc.expression)
|| sc.statements.iter().any(|s| stmt_has_get_defined_vars(s))
}
SwitchCase::Default(dc) => {
dc.statements.iter().any(|s| stmt_has_get_defined_vars(s))
}
})
}
Statement::Try(try_stmt) => {
try_stmt
.block
.statements
.iter()
.any(|s| stmt_has_get_defined_vars(s))
|| try_stmt.catch_clauses.iter().any(|c| {
c.block
.statements
.iter()
.any(|s| stmt_has_get_defined_vars(s))
})
|| try_stmt.finally_clause.as_ref().is_some_and(|f| {
f.block
.statements
.iter()
.any(|s| stmt_has_get_defined_vars(s))
})
}
Statement::Block(block) => block
.statements
.iter()
.any(|s| stmt_has_get_defined_vars(s)),
_ => false,
}
}

fn expr_has_get_defined_vars(expr: &Expression<'_>) -> bool {
match expr {
Expression::Call(Call::Function(fc)) => {
if let Expression::Identifier(ident) = fc.function
&& ident.value().eq_ignore_ascii_case(b"get_defined_vars")
{
return true;
}
expr_has_get_defined_vars(fc.function)
// Recurse into arguments for nested calls.
|| fc
.argument_list
.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
}
Expression::Call(Call::Method(mc)) => {
expr_has_get_defined_vars(mc.object)
|| mc
.argument_list
.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
}
Expression::Call(Call::NullSafeMethod(mc)) => {
expr_has_get_defined_vars(mc.object)
|| mc
.argument_list
.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
}
Expression::Call(Call::StaticMethod(sc)) => {
expr_has_get_defined_vars(sc.class)
|| sc
.argument_list
.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
}
Expression::Access(access) => match access {
Access::Property(pa) => expr_has_get_defined_vars(pa.object),
Access::NullSafeProperty(pa) => expr_has_get_defined_vars(pa.object),
Access::StaticProperty(spa) => expr_has_get_defined_vars(spa.class),
Access::ClassConstant(cca) => expr_has_get_defined_vars(cca.class),
},
Expression::ArrayAccess(aa) => {
expr_has_get_defined_vars(aa.array) || expr_has_get_defined_vars(aa.index)
}
Expression::ArrayAppend(append) => expr_has_get_defined_vars(append.array),
Expression::Array(arr) => arr
.elements
.iter()
.any(|e| array_elem_has_get_defined_vars(e)),
Expression::LegacyArray(arr) => arr
.elements
.iter()
.any(|e| array_elem_has_get_defined_vars(e)),
Expression::List(list) => list
.elements
.iter()
.any(|e| array_elem_has_get_defined_vars(e)),
Expression::Assignment(a) => {
expr_has_get_defined_vars(a.lhs) || expr_has_get_defined_vars(a.rhs)
}
Expression::Binary(b) => {
expr_has_get_defined_vars(b.lhs) || expr_has_get_defined_vars(b.rhs)
}
Expression::UnaryPrefix(u) => expr_has_get_defined_vars(u.operand),
Expression::UnaryPostfix(u) => expr_has_get_defined_vars(u.operand),
Expression::Parenthesized(p) => expr_has_get_defined_vars(p.expression),
Expression::Conditional(c) => {
expr_has_get_defined_vars(c.condition)
|| c.then.is_some_and(|t| expr_has_get_defined_vars(t))
|| expr_has_get_defined_vars(c.r#else)
}
Expression::Instantiation(inst) => {
expr_has_get_defined_vars(inst.class)
|| inst.argument_list.as_ref().is_some_and(|al| {
al.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
})
}
Expression::Throw(t) => expr_has_get_defined_vars(t.exception),
Expression::Clone(c) => expr_has_get_defined_vars(c.object),
Expression::Yield(yield_expr) => match yield_expr {
Yield::Value(yv) => yv.value.is_some_and(expr_has_get_defined_vars),
Yield::Pair(yp) => {
expr_has_get_defined_vars(yp.key) || expr_has_get_defined_vars(yp.value)
}
Yield::From(yf) => expr_has_get_defined_vars(yf.iterator),
},
Expression::Match(m) => {
expr_has_get_defined_vars(m.expression)
|| m.arms.iter().any(|arm| match arm {
MatchArm::Expression(ea) => {
ea.conditions.iter().any(|c| expr_has_get_defined_vars(c))
|| expr_has_get_defined_vars(ea.expression)
}
MatchArm::Default(da) => expr_has_get_defined_vars(da.expression),
})
}
Expression::Construct(construct) => match construct {
Construct::Isset(isset) => isset.values.iter().any(|v| expr_has_get_defined_vars(v)),
Construct::Empty(empty) => expr_has_get_defined_vars(empty.value),
Construct::Eval(eval) => expr_has_get_defined_vars(eval.value),
Construct::Include(inc) => expr_has_get_defined_vars(inc.value),
Construct::IncludeOnce(inc) => expr_has_get_defined_vars(inc.value),
Construct::Require(req) => expr_has_get_defined_vars(req.value),
Construct::RequireOnce(req) => expr_has_get_defined_vars(req.value),
Construct::Print(print) => expr_has_get_defined_vars(print.value),
Construct::Exit(exit) => exit.arguments.as_ref().is_some_and(|args| {
args.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
}),
Construct::Die(die) => die.arguments.as_ref().is_some_and(|args| {
args.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
}),
},
Expression::CompositeString(composite) => composite.parts().iter().any(|part| match part {
StringPart::Expression(inner_expr) => expr_has_get_defined_vars(inner_expr),
StringPart::BracedExpression(braced) => expr_has_get_defined_vars(braced.expression),
StringPart::Literal(_) => false,
}),
Expression::Pipe(pipe) => {
expr_has_get_defined_vars(pipe.input) || expr_has_get_defined_vars(pipe.callable)
}
Expression::PartialApplication(partial) => match partial {
PartialApplication::Function(func_pa) => expr_has_get_defined_vars(func_pa.function),
PartialApplication::Method(method_pa) => expr_has_get_defined_vars(method_pa.object),
PartialApplication::StaticMethod(static_pa) => {
expr_has_get_defined_vars(static_pa.class)
}
},
Expression::AnonymousClass(anon) => anon.argument_list.as_ref().is_some_and(|args| {
args.arguments
.iter()
.any(|a| expr_has_get_defined_vars(a.value()))
}),
// Don't recurse into closures/arrow functions.
Expression::Closure(_) | Expression::ArrowFunction(_) => false,
_ => false,
}
}

fn array_elem_has_get_defined_vars(elem: &ArrayElement<'_>) -> bool {
match elem {
ArrayElement::KeyValue(kv) => {
expr_has_get_defined_vars(kv.key) || expr_has_get_defined_vars(kv.value)
}
ArrayElement::Value(v) => expr_has_get_defined_vars(v.value),
ArrayElement::Variadic(s) => expr_has_get_defined_vars(s.value),
ArrayElement::Missing(_) => false,
}
}

// ─── @var annotation collection ─────────────────────────────────────────────

/// Scan the source text for `/** @var Type $varName */` inline
Expand Down
Loading
Loading