use php_ast::*;
use php_lexer::TokenKind;
use crate::diagnostics::{ForbiddenRule, ParseError};
use crate::instrument;
use crate::parser::Parser;
use crate::precedence::{
self, ASSIGNMENT_BP, MEMBER_ACCESS_BP, NULL_COALESCE_LEFT_BP, SCOPE_RESOLUTION_BP, TERNARY_BP,
};
use crate::version::PhpVersion;
use php_ast::span::Span;
mod atom;
pub(crate) mod interpolation;
pub(crate) use atom::{
parse_arg_list_or_callable, parse_arrow_function, parse_closure, ArgListResult,
};
use atom::{parse_atom, parse_function_call};
fn nonassoc_chain_level_for_token(kind: TokenKind) -> Option<u8> {
match kind {
TokenKind::EqualsEquals
| TokenKind::BangEquals
| TokenKind::EqualsEqualsEquals
| TokenKind::BangEqualsEquals
| TokenKind::Spaceship => Some(25),
TokenKind::LessThan
| TokenKind::GreaterThan
| TokenKind::LessThanEquals
| TokenKind::GreaterThanEquals => Some(27),
_ => None,
}
}
fn nonassoc_chain_level_for_op(op: BinaryOp) -> Option<u8> {
match op {
BinaryOp::Equal
| BinaryOp::NotEqual
| BinaryOp::Identical
| BinaryOp::NotIdentical
| BinaryOp::Spaceship => Some(25),
BinaryOp::Less | BinaryOp::Greater | BinaryOp::LessOrEqual | BinaryOp::GreaterOrEqual => {
Some(27)
}
_ => None,
}
}
fn is_valid_assignment_target(kind: &ExprKind<'_, '_>) -> bool {
matches!(
kind,
ExprKind::Variable(_)
| ExprKind::VariableVariable(_)
| ExprKind::ArrayAccess(_)
| ExprKind::PropertyAccess(_)
| ExprKind::NullsafePropertyAccess(_)
| ExprKind::StaticPropertyAccess(_)
| ExprKind::StaticPropertyAccessDynamic { .. }
| ExprKind::Array(_)
| ExprKind::Error
)
}
pub(crate) fn check_increment_operand(parser: &mut Parser<'_, '_>, operand: &Expr<'_, '_>) {
let (rule, message) = if !matches!(
operand.kind,
ExprKind::Variable(_)
| ExprKind::VariableVariable(_)
| ExprKind::ArrayAccess(_)
| ExprKind::PropertyAccess(_)
| ExprKind::NullsafePropertyAccess(_)
| ExprKind::StaticPropertyAccess(_)
| ExprKind::StaticPropertyAccessDynamic { .. }
| ExprKind::Error
) {
(
ForbiddenRule::IncDecNonVariable,
"Cannot increment or decrement an expression that is not a variable",
)
} else if let Some(error) = temporary_write_message(&operand.kind, parser.version) {
error
} else if has_nullsafe_link(&operand.kind) {
(
ForbiddenRule::NullsafeWrite,
"Can't use nullsafe operator in write context",
)
} else {
return check_special_variable_write(parser, operand, WriteKind::Modify);
};
parser.forbid(rule, message, operand.span);
}
pub(crate) fn check_write_target(
parser: &mut Parser<'_, '_>,
target: &Expr<'_, '_>,
allow_list: bool,
) {
let (rule, message) = if let Some(error) = temporary_write_message(&target.kind, parser.version)
{
error
} else if has_nullsafe_link(&target.kind) {
(
ForbiddenRule::NullsafeWrite,
"Can't use nullsafe operator in write context",
)
} else if !is_valid_assignment_target(&target.kind)
|| !allow_list && matches!(target.kind, ExprKind::Array(_))
{
(
ForbiddenRule::InvalidWriteContext,
"Can't use this expression in write context",
)
} else if !is_valid_destructure_element(&target.kind, parser.version) {
(
ForbiddenRule::UnwritableDestructuringElement,
"Assignments can only happen to writable values",
)
} else {
return;
};
parser.forbid(rule, message, target.span);
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum WriteKind {
Assign,
Modify,
Reference,
Unset,
}
pub(crate) fn check_special_variable_write(
parser: &mut Parser<'_, '_>,
target: &Expr<'_, '_>,
kind: WriteKind,
) {
let (rule, message) = match &target.kind {
ExprKind::Array(elems) if kind == WriteKind::Assign => {
for el in elems.iter() {
let mut value = el.value();
while let ExprKind::Parenthesized(inner) = &value.kind {
value = inner;
}
if !el.is_by_ref() || is_variable_named(value, "this") {
check_special_variable_write(parser, value, kind);
}
}
return;
}
ExprKind::Variable(name) => match (name.as_str(), kind) {
("this", WriteKind::Assign) => (ForbiddenRule::ReassignThis, "Cannot re-assign $this"),
("this", WriteKind::Unset) => (ForbiddenRule::UnsetThis, "Cannot unset $this"),
("GLOBALS", _) if parser.version < PhpVersion::Php81 => return,
("GLOBALS", WriteKind::Reference) => (
ForbiddenRule::InvalidGlobalsWrite,
"Cannot acquire reference to $GLOBALS",
),
("GLOBALS", _) => (
ForbiddenRule::InvalidGlobalsWrite,
"$GLOBALS can only be modified using the $GLOBALS[$name] = $value syntax",
),
_ => return,
},
_ => return,
};
parser.forbid(rule, message, target.span);
}
fn is_variable_named(expr: &Expr<'_, '_>, name: &str) -> bool {
matches!(&expr.kind, ExprKind::Variable(n) if n.as_str() == name)
}
pub(crate) fn is_superglobal(name: &str) -> bool {
matches!(
name,
"GLOBALS"
| "_GET"
| "_POST"
| "_COOKIE"
| "_FILES"
| "_SERVER"
| "_ENV"
| "_REQUEST"
| "_SESSION"
)
}
fn is_clone_keyword(name: &Expr<'_, '_>) -> bool {
matches!(&name.kind, ExprKind::Name(Name::Simple { value, .. }) if value.eq_ignore_ascii_case("clone"))
}
fn check_dereferencable(parser: &mut Parser<'_, '_>, lhs: &Expr<'_, '_>, op: TokenKind) {
let dereferencable = match lhs.kind {
ExprKind::Match(_)
| ExprKind::Closure(_)
| ExprKind::ArrowFunction(_)
| ExprKind::Int(_)
| ExprKind::Float(_)
| ExprKind::Heredoc { .. }
| ExprKind::Nowdoc { .. }
| ExprKind::ShellExec(_)
| ExprKind::Isset(_)
| ExprKind::Empty(_)
| ExprKind::Exit(_)
| ExprKind::Eval(_) => false,
ExprKind::Array(_) => !is_list_syntax(parser.source(), lhs.span),
ExprKind::MagicConst(_) => !matches!(op, TokenKind::DoubleColon | TokenKind::LeftParen),
ExprKind::CloneWith(..) | ExprKind::Clone(_) => false,
ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::Function(name),
})
| ExprKind::FunctionCall(FunctionCallExpr { name, .. }) => !is_clone_keyword(name),
_ => true,
};
if !dereferencable {
parser.forbid(
ForbiddenRule::InvalidDereference,
"Cannot dereference this expression",
lhs.span,
);
} else if parser.version < PhpVersion::Php80 {
let object_access = matches!(op, TokenKind::Arrow | TokenKind::NullsafeArrow);
let feature = match lhs.kind {
ExprKind::Name(_) if object_access => "object access on a constant",
ExprKind::ClassConstAccess(_) if object_access || op == TokenKind::DoubleColon => {
"class constant dereferencing"
}
ExprKind::MagicConst(_) => "magic constant dereferencing",
ExprKind::InterpolatedString(_) => "interpolated string dereferencing",
_ => return,
};
parser.require_version(PhpVersion::Php80, feature, lhs.span);
}
}
pub(crate) const CURLY_OFFSET_REMOVED: &str =
"Array and string offset access syntax with curly braces is no longer supported";
fn is_class_name_reference(expr: &Expr<'_, '_>) -> bool {
matches!(
expr.kind,
ExprKind::Name(_) | ExprKind::Parenthesized(_) | ExprKind::Error
) || is_new_variable(expr)
}
fn is_new_variable(expr: &Expr<'_, '_>) -> bool {
match &expr.kind {
ExprKind::Variable(_) | ExprKind::VariableVariable(_) => true,
ExprKind::ArrayAccess(a) => is_new_variable(a.array),
ExprKind::PropertyAccess(p) | ExprKind::NullsafePropertyAccess(p) => {
is_new_variable(p.object)
}
ExprKind::StaticPropertyAccess(StaticAccessExpr { class, .. })
| ExprKind::StaticPropertyAccessDynamic { class, .. } => {
matches!(class.kind, ExprKind::Name(_)) || is_new_variable(class)
}
_ => false,
}
}
pub(crate) fn check_class_name_literal(parser: &mut Parser<'_, '_>, class: &Expr<'_, '_>) {
if is_literal(class) && literal_is_string(class, parser.version) == Some(false) {
parser.forbid(
ForbiddenRule::LiteralClassName,
"Illegal class name",
class.span,
);
}
}
fn check_class_name_fetch_literal(parser: &mut Parser<'_, '_>, class: &Expr<'_, '_>) {
let (rule, message) = match &unparenthesized(class).kind {
ExprKind::Int(_) | ExprKind::Float(_) => {
(ForbiddenRule::LiteralClassName, "Illegal class name")
}
ExprKind::Bool(true) => (
ForbiddenRule::ClassFetchOnLiteral,
"Cannot use \"::class\" on true",
),
ExprKind::Bool(false) => (
ForbiddenRule::ClassFetchOnLiteral,
"Cannot use \"::class\" on false",
),
ExprKind::Null => (
ForbiddenRule::ClassFetchOnLiteral,
"Cannot use \"::class\" on null",
),
ExprKind::Array(_) if is_literal(class) => (
ForbiddenRule::ClassFetchOnLiteral,
"Cannot use \"::class\" on array",
),
_ => return,
};
parser.forbid(rule, message, class.span);
}
fn literal_is_string(expr: &Expr<'_, '_>, version: PhpVersion) -> Option<bool> {
folded_type(expr, version).map(|t| matches!(t, Folded::Str { .. }))
}
#[derive(Clone, Copy)]
enum Folded {
Int,
Number,
Bool,
Null,
Str {
numeric: bool,
},
Array,
}
impl Folded {
fn is_arithmetic(self) -> bool {
!matches!(self, Folded::Array | Folded::Str { numeric: false })
}
fn is_integral(self, version: PhpVersion) -> bool {
matches!(self, Folded::Int | Folded::Bool | Folded::Null)
|| (matches!(self, Folded::Number) && version < PhpVersion::Php81)
}
}
fn folded_type(expr: &Expr<'_, '_>, version: PhpVersion) -> Option<Folded> {
let non_negative_int =
|e: &Expr<'_, '_>| matches!(unparenthesized(e).kind, ExprKind::Int(n) if n >= 0);
let folded = match &expr.kind {
ExprKind::String(s) => Folded::Str {
numeric: is_numeric_string(s.as_bytes()),
},
ExprKind::Nowdoc { value, .. } => Folded::Str {
numeric: is_numeric_string(value.as_bytes()),
},
ExprKind::Heredoc { .. } => Folded::Str { numeric: false },
ExprKind::Int(_) => Folded::Int,
ExprKind::Float(_) => Folded::Number,
ExprKind::Bool(_) => Folded::Bool,
ExprKind::Null => Folded::Null,
ExprKind::Array(_) => Folded::Array,
ExprKind::Parenthesized(inner) => return folded_type(inner, version),
ExprKind::UnaryPrefix(u) => {
let operand = folded_type(u.operand, version)?;
match u.op {
UnaryPrefixOp::BooleanNot => Folded::Bool,
UnaryPrefixOp::Negate | UnaryPrefixOp::Plus if operand.is_arithmetic() => {
Folded::Number
}
UnaryPrefixOp::BitwiseNot => match operand {
Folded::Str { .. } => Folded::Str { numeric: false },
Folded::Int => Folded::Int,
Folded::Number if version < PhpVersion::Php81 => Folded::Int,
_ => return None,
},
_ => return None,
}
}
ExprKind::Binary(b) => {
let (left, right) = (
folded_type(b.left, version)?,
folded_type(b.right, version)?,
);
match b.op {
BinaryOp::Concat
if !matches!(left, Folded::Array) && !matches!(right, Folded::Array) =>
{
Folded::Str { numeric: false }
}
BinaryOp::Add if matches!((left, right), (Folded::Array, Folded::Array)) => {
Folded::Array
}
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul
if left.is_arithmetic() && right.is_arithmetic() =>
{
Folded::Number
}
BinaryOp::Pow
if left.is_arithmetic()
&& right.is_arithmetic()
&& (version < PhpVersion::Php84 || non_negative_int(b.right)) =>
{
Folded::Number
}
BinaryOp::ShiftLeft | BinaryOp::ShiftRight
if left.is_integral(version) && non_negative_int(b.right) =>
{
Folded::Int
}
BinaryOp::BitwiseAnd | BinaryOp::BitwiseOr | BinaryOp::BitwiseXor => {
match (left, right) {
(Folded::Str { .. }, Folded::Str { .. }) => Folded::Str { numeric: false },
_ if left.is_integral(version) && right.is_integral(version) => Folded::Int,
_ => return None,
}
}
BinaryOp::Equal
| BinaryOp::NotEqual
| BinaryOp::Identical
| BinaryOp::NotIdentical
| BinaryOp::Less
| BinaryOp::Greater
| BinaryOp::LessOrEqual
| BinaryOp::GreaterOrEqual
| BinaryOp::BooleanAnd
| BinaryOp::BooleanOr
| BinaryOp::LogicalAnd
| BinaryOp::LogicalOr
| BinaryOp::LogicalXor => Folded::Bool,
BinaryOp::Spaceship => Folded::Int,
_ => return None,
}
}
_ => return None,
};
Some(folded)
}
fn is_numeric_string(s: &[u8]) -> bool {
let is_space = |b: &u8| matches!(b, b' ' | b'\t' | b'\n' | b'\r' | b'\x0B' | b'\x0C');
let start = s.iter().position(|b| !is_space(b)).unwrap_or(s.len());
let end = s
.iter()
.rposition(|b| !is_space(b))
.map_or(start, |p| p + 1);
let mut rest = s[start..end]
.strip_prefix(b"-")
.or_else(|| s[start..end].strip_prefix(b"+"))
.unwrap_or(&s[start..end]);
let digits = |rest: &mut &[u8]| {
let n = rest.iter().take_while(|b| b.is_ascii_digit()).count();
*rest = &rest[n..];
n
};
let mut mantissa = digits(&mut rest);
if let Some(after) = rest.strip_prefix(b".") {
rest = after;
mantissa += digits(&mut rest);
}
if mantissa == 0 {
return false;
}
if let Some(after) = rest.strip_prefix(b"e").or_else(|| rest.strip_prefix(b"E")) {
rest = after
.strip_prefix(b"-")
.or_else(|| after.strip_prefix(b"+"))
.unwrap_or(after);
if digits(&mut rest) == 0 {
return false;
}
}
rest.is_empty()
}
fn has_nullsafe_link(kind: &ExprKind<'_, '_>) -> bool {
nullsafe_link(kind, false)
}
fn is_this(kind: &ExprKind<'_, '_>) -> bool {
match kind {
ExprKind::Variable(n) => n.as_str() == "this",
ExprKind::Parenthesized(inner) => is_this(&inner.kind),
_ => false,
}
}
fn nullsafe_link(kind: &ExprKind<'_, '_>, this_is_exempt: bool) -> bool {
let mut cur = kind;
loop {
cur = match cur {
ExprKind::NullsafePropertyAccess(a) if this_is_exempt && is_this(&a.object.kind) => {
return false;
}
ExprKind::NullsafeMethodCall(c) if this_is_exempt && is_this(&c.object.kind) => {
return false;
}
ExprKind::NullsafePropertyAccess(_) | ExprKind::NullsafeMethodCall(_) => return true,
ExprKind::PropertyAccess(a) => &a.object.kind,
ExprKind::MethodCall(c) => &c.object.kind,
ExprKind::ArrayAccess(a) => &a.array.kind,
ExprKind::StaticPropertyAccess(a) => &a.class.kind,
ExprKind::StaticPropertyAccessDynamic { class, .. } => &class.kind,
ExprKind::StaticMethodCall(c) => &c.class.kind,
ExprKind::StaticMethodCallDynamic(c) => &c.class.kind,
ExprKind::Parenthesized(inner) => &inner.kind,
_ => return false,
};
}
}
fn is_valid_destructure_element(kind: &ExprKind<'_, '_>, version: PhpVersion) -> bool {
match kind {
ExprKind::Omit => true,
ExprKind::Array(elems) => elems
.iter()
.all(|el| !el.is_spread() && is_valid_destructure_element(&el.value().kind, version)),
ExprKind::Parenthesized(inner) => is_valid_destructure_element(&inner.kind, version),
_ => {
is_valid_assignment_target(kind)
&& !has_nullsafe_link(kind)
&& temporary_write_message(kind, version).is_none()
}
}
}
pub(crate) fn check_reference_return(parser: &mut Parser<'_, '_>, value: &Expr<'_, '_>) {
if !parser.by_ref_function {
return;
}
let mut kind = &value.kind;
while let ExprKind::Parenthesized(inner) = kind {
kind = &inner.kind;
}
let is_fetch = matches!(
kind,
ExprKind::ArrayAccess(_)
| ExprKind::PropertyAccess(_)
| ExprKind::NullsafePropertyAccess(_)
| ExprKind::StaticPropertyAccess(_)
| ExprKind::StaticPropertyAccessDynamic { .. }
);
if is_fetch && has_nullsafe_link(kind) {
parser.forbid(
ForbiddenRule::ReferenceToNullsafe,
"Cannot take reference of a nullsafe chain",
value.span,
);
}
}
pub(crate) fn is_variable_or_call(kind: &ExprKind<'_, '_>) -> bool {
matches!(
kind,
ExprKind::Variable(_)
| ExprKind::VariableVariable(_)
| ExprKind::ArrayAccess(_)
| ExprKind::PropertyAccess(_)
| ExprKind::NullsafePropertyAccess(_)
| ExprKind::StaticPropertyAccess(_)
| ExprKind::StaticPropertyAccessDynamic { .. }
| ExprKind::FunctionCall(_)
| ExprKind::MethodCall(_)
| ExprKind::NullsafeMethodCall(_)
| ExprKind::StaticMethodCall(_)
| ExprKind::StaticMethodCallDynamic(_)
| ExprKind::CallableCreate(_)
| ExprKind::Error
)
}
fn has_reference_element(kind: &ExprKind<'_, '_>) -> bool {
let ExprKind::Array(elems) = kind else {
return false;
};
elems
.iter()
.any(|el| el.is_by_ref() || has_reference_element(&el.value().kind))
}
const INVALID_CONST_EXPR: &str = "Constant expression contains invalid operations";
enum ConstExprError {
Forbidden(&'static str, Span),
Version(PhpVersion, &'static str, Span),
}
pub(crate) fn check_const_expr(parser: &mut Parser<'_, '_>, expr: &Expr<'_, '_>, allow_new: bool) {
let checker = ConstExprChecker {
version: parser.version,
allow_new,
};
match checker.check(expr) {
Ok(()) => {}
Err(ConstExprError::Forbidden(message, span)) => {
parser.forbid(ForbiddenRule::InvalidConstantExpression, message, span)
}
Err(ConstExprError::Version(min, feature, span)) => {
parser.require_version(min, feature, span)
}
}
}
struct ConstExprChecker {
version: PhpVersion,
allow_new: bool,
}
impl ConstExprChecker {
fn require(
&self,
min: PhpVersion,
feature: &'static str,
span: Span,
) -> Result<(), ConstExprError> {
if self.version < min {
return Err(ConstExprError::Version(min, feature, span));
}
Ok(())
}
fn check(&self, expr: &Expr<'_, '_>) -> Result<(), ConstExprError> {
use ConstExprError::Forbidden;
match &expr.kind {
ExprKind::Int(_)
| ExprKind::Float(_)
| ExprKind::String(_)
| ExprKind::Bool(_)
| ExprKind::Null
| ExprKind::Nowdoc { .. }
| ExprKind::MagicConst(_)
| ExprKind::Name(_)
| ExprKind::Identifier(_)
| ExprKind::Omit
| ExprKind::Error => Ok(()),
ExprKind::Heredoc { parts, .. } if is_literal_parts(parts) => Ok(()),
ExprKind::Parenthesized(inner) => self.check(inner),
ExprKind::UnaryPrefix(u)
if !matches!(
u.op,
UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement
) =>
{
self.check(u.operand)
}
ExprKind::Binary(b) if !matches!(b.op, BinaryOp::Instanceof | BinaryOp::Pipe) => {
self.check(b.left)?;
let folded_away = match (b.op, folded_truth(b.left)) {
(BinaryOp::BooleanAnd | BinaryOp::LogicalAnd, Some(truth)) => !truth,
(BinaryOp::BooleanOr | BinaryOp::LogicalOr, Some(truth)) => truth,
(
BinaryOp::BooleanAnd
| BinaryOp::BooleanOr
| BinaryOp::LogicalAnd
| BinaryOp::LogicalOr,
None,
) => may_fold(b.left),
_ => false,
};
if folded_away {
return Ok(());
}
self.check(b.right)
}
ExprKind::Ternary(t) => {
self.check(t.condition)?;
match folded_truth(t.condition) {
Some(true) => t
.then_expr
.map_or(Ok(()), |then_expr| self.check(then_expr)),
Some(false) => self.check(t.else_expr),
None if may_fold(t.condition) => Ok(()),
None => {
if let Some(then_expr) = t.then_expr {
self.check(then_expr)?;
}
self.check(t.else_expr)
}
}
}
ExprKind::NullCoalesce(n) => {
self.check(n.left)?;
let left_is_null = matches!(unparenthesized(n.left).kind, ExprKind::Null);
if !left_is_null && may_fold(n.left) {
return Ok(());
}
self.check(n.right)
}
ExprKind::Array(elements) => {
for el in elements.iter() {
if el.is_by_ref() {
return Err(Forbidden(INVALID_CONST_EXPR, el.span));
}
if let Some(key) = &el.key {
self.check(key)?;
}
self.check(el.value())?;
}
Ok(())
}
ExprKind::ArrayAccess(a) => {
self.check(a.array)?;
match a.index {
Some(index) => self.check(index),
None => Ok(()),
}
}
ExprKind::Cast(kind, inner) => match kind {
CastKind::Unset | CastKind::Void => Ok(()),
_ => {
self.require(
PhpVersion::Php85,
"casts in constant expressions",
expr.span,
)?;
if *kind == CastKind::Object && !self.allow_new {
return Err(Forbidden(
"Object casts are not supported in this context",
expr.span,
));
}
self.check(inner)
}
},
ExprKind::PropertyAccess(p) | ExprKind::NullsafePropertyAccess(p) => {
self.require(
PhpVersion::Php82,
"property fetch in constant expressions",
expr.span,
)?;
self.check(p.object)?;
self.check(p.property)
}
ExprKind::ClassConstAccess(access) => {
let class = access.class;
let is_class_name = matches!(&access.member.kind, ExprKind::Identifier(name) if name.eq_ignore_ascii_case("class"));
if !is_class_name {
return self.class_const_class(class);
}
match &class.kind {
ExprKind::Name(_) => Ok(()),
_ if is_literal(class) => Ok(()),
_ => Err(Forbidden(
"(expression)::class cannot be used in constant expressions",
expr.span,
)),
}
}
ExprKind::ClassConstAccessDynamic { class, member } => {
self.class_const_class(class)?;
self.check(member)
}
ExprKind::New(new) => {
if !self.allow_new {
return Err(Forbidden(
"New expressions are not supported in this context",
expr.span,
));
}
self.require(PhpVersion::Php81, "new in initializers", expr.span)?;
let class = new.class;
match &class.kind {
ExprKind::Name(name) if is_static_name(name) => {
return Err(Forbidden(
"\"static\" is not allowed in compile-time constants",
class.span,
))
}
ExprKind::Name(_) => {}
ExprKind::AnonymousClass(_) => {
return Err(Forbidden(
"Cannot use anonymous class in constant expression",
class.span,
))
}
_ if may_fold(class) => {}
_ => {
return Err(Forbidden(
"Cannot use dynamic class name in constant expression",
class.span,
))
}
}
for arg in new.args.iter() {
if arg.is_spread() {
return Err(Forbidden(
"Argument unpacking in constant expressions is not supported",
arg.span,
));
}
if let Some(value) = arg.value() {
self.check(value)?;
}
}
Ok(())
}
ExprKind::Closure(closure) => {
self.require(
PhpVersion::Php85,
"closures in constant expressions",
expr.span,
)?;
if !closure.is_static {
return Err(Forbidden(
"Closures in constant expressions must be static",
expr.span,
));
}
if !closure.use_vars.is_empty() {
return Err(Forbidden(
"Cannot use(...) variables in constant expression",
expr.span,
));
}
Ok(())
}
ExprKind::CallableCreate(cc) => {
self.require(
PhpVersion::Php85,
"first-class callables in constant expressions",
expr.span,
)?;
match &cc.kind {
CallableCreateKind::Function(name) => self.callable_function(name),
CallableCreateKind::StaticMethod { class, method } => {
self.callable_static_method(class, method)
}
_ => Err(Forbidden(INVALID_CONST_EXPR, expr.span)),
}
}
ExprKind::FunctionCall(call)
if self.version >= PhpVersion::Php86
&& call.args.iter().any(Arg::is_placeholder) =>
{
self.callable_function(call.name)?;
self.partial_args(&call.args)
}
ExprKind::StaticMethodCall(call)
if self.version >= PhpVersion::Php86
&& call.args.iter().any(Arg::is_placeholder) =>
{
self.callable_static_method(call.class, call.method)?;
self.partial_args(&call.args)
}
_ => Err(Forbidden(INVALID_CONST_EXPR, expr.span)),
}
}
fn callable_function(&self, name: &Expr<'_, '_>) -> Result<(), ConstExprError> {
if matches!(name.kind, ExprKind::Name(_)) || is_literal(name) {
Ok(())
} else {
Err(ConstExprError::Forbidden(
"Cannot use dynamic function name in constant expression",
name.span,
))
}
}
fn callable_static_method(
&self,
class: &Expr<'_, '_>,
method: &Expr<'_, '_>,
) -> Result<(), ConstExprError> {
match &class.kind {
ExprKind::Name(name) if is_static_name(name) => {
return Err(ConstExprError::Forbidden(
"\"static\" is not allowed in compile-time constants",
class.span,
))
}
ExprKind::Name(_) => {}
_ if may_fold(class) => {}
_ => {
return Err(ConstExprError::Forbidden(
"Cannot use dynamic class name in constant expression",
class.span,
))
}
}
if matches!(method.kind, ExprKind::Identifier(_)) || is_literal(method) {
Ok(())
} else {
Err(ConstExprError::Forbidden(
"Cannot use dynamic method name in constant expression",
method.span,
))
}
}
fn partial_args(&self, args: &[Arg<'_, '_>]) -> Result<(), ConstExprError> {
for arg in args {
match &arg.kind {
ArgKind::Value(value) => self.check(value)?,
ArgKind::Placeholder | ArgKind::SpreadPlaceholder => {}
ArgKind::Spread(_) => {
return Err(ConstExprError::Forbidden(
"Argument unpacking in constant expressions is not supported",
arg.span,
))
}
_ => return Err(ConstExprError::Forbidden(INVALID_CONST_EXPR, arg.span)),
}
}
Ok(())
}
fn class_const_class(&self, class: &Expr<'_, '_>) -> Result<(), ConstExprError> {
match &class.kind {
ExprKind::Name(name) if is_static_name(name) => Err(ConstExprError::Forbidden(
"\"static::\" is not allowed in compile-time constants",
class.span,
)),
ExprKind::Name(_) => Ok(()),
_ if may_fold(class) => Ok(()),
_ => Err(ConstExprError::Forbidden(
"Dynamic class names are not allowed in compile-time class constant references",
class.span,
)),
}
}
}
fn is_class_keyword_name(name: &Name<'_, '_>) -> bool {
matches!(name, Name::Simple { value, .. }
if ["self", "parent", "static"].iter().any(|k| value.eq_ignore_ascii_case(k)))
}
fn is_static_name(name: &Name<'_, '_>) -> bool {
matches!(name, Name::Simple { value, .. } if value.eq_ignore_ascii_case("static"))
}
fn is_literal_parts(parts: &[StringPart<'_, '_>]) -> bool {
parts
.iter()
.all(|part| matches!(part, StringPart::Literal(_)))
}
fn is_literal(expr: &Expr<'_, '_>) -> bool {
foldable(expr, false)
}
fn may_fold(expr: &Expr<'_, '_>) -> bool {
foldable(expr, true)
}
fn unparenthesized<'e, 'arena, 'src>(mut expr: &'e Expr<'arena, 'src>) -> &'e Expr<'arena, 'src> {
while let ExprKind::Parenthesized(inner) = &expr.kind {
expr = inner;
}
expr
}
fn is_short_circuit(op: BinaryOp) -> bool {
matches!(
op,
BinaryOp::BooleanAnd | BinaryOp::BooleanOr | BinaryOp::LogicalAnd | BinaryOp::LogicalOr
)
}
fn folded_truth(expr: &Expr<'_, '_>) -> Option<bool> {
let expr = unparenthesized(expr);
match &expr.kind {
ExprKind::Int(n) => Some(*n != 0),
ExprKind::Float(f) => Some(*f != 0.0),
ExprKind::String(s) => Some(!matches!(s.as_bytes(), b"" | b"0")),
ExprKind::Nowdoc { value, .. } => Some(!matches!(*value, "" | "0")),
ExprKind::Bool(b) => Some(*b),
ExprKind::Null => Some(false),
ExprKind::UnaryPrefix(u) if u.op == UnaryPrefixOp::BooleanNot => {
folded_truth(u.operand).map(|truth| !truth)
}
ExprKind::Array(elements) if is_literal(expr) => {
let mut truth = false;
for el in elements.iter() {
match &el.kind {
ArrayElementKind::Spread(inner) => truth |= folded_truth(inner)?,
_ => return Some(true),
}
}
Some(truth)
}
ExprKind::ClassConstAccess(access) => {
let resolved = matches!(&access.member.kind, ExprKind::Identifier(m) if m.eq_ignore_ascii_case("class"))
&& matches!(&access.class.kind, ExprKind::Name(name) if !is_class_keyword_name(name));
resolved.then_some(true)
}
ExprKind::Binary(b) if is_short_circuit(b.op) => {
let mut spine = vec![b];
while let ExprKind::Binary(inner) = &unparenthesized(spine[spine.len() - 1].left).kind {
if !is_short_circuit(inner.op) {
break;
}
spine.push(inner);
}
let mut truth = folded_truth(spine[spine.len() - 1].left)?;
for link in spine.iter().rev() {
let decided = match link.op {
BinaryOp::BooleanAnd | BinaryOp::LogicalAnd => !truth,
_ => truth,
};
if !decided {
truth = folded_truth(link.right)?;
}
}
Some(truth)
}
_ => None,
}
}
fn foldable(expr: &Expr<'_, '_>, constants: bool) -> bool {
let operand = |e: &Expr<'_, '_>| foldable(e, constants);
match &expr.kind {
ExprKind::Int(_)
| ExprKind::Float(_)
| ExprKind::String(_)
| ExprKind::Bool(_)
| ExprKind::Null
| ExprKind::Nowdoc { .. } => true,
ExprKind::Heredoc { parts, .. } => is_literal_parts(parts),
ExprKind::MagicConst(_) | ExprKind::Name(_) => constants,
ExprKind::ClassConstAccess(access) => {
constants
&& matches!(access.class.kind, ExprKind::Name(_))
&& matches!(access.member.kind, ExprKind::Identifier(_))
}
ExprKind::Parenthesized(inner) => operand(inner),
ExprKind::UnaryPrefix(u) => {
!matches!(
u.op,
UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement
) && operand(u.operand)
}
ExprKind::Binary(b) => {
!matches!(b.op, BinaryOp::Instanceof | BinaryOp::Pipe)
&& operand(b.left)
&& operand(b.right)
}
ExprKind::Ternary(t) => {
operand(t.condition) && t.then_expr.is_none_or(operand) && operand(t.else_expr)
}
ExprKind::NullCoalesce(n) => operand(n.left) && operand(n.right),
ExprKind::Array(elements) => elements.iter().all(|el| {
!el.is_by_ref() && el.key.as_ref().is_none_or(operand) && operand(el.value())
}),
ExprKind::ArrayAccess(a) => operand(a.array) && a.index.is_some_and(operand),
_ => false,
}
}
pub(crate) fn check_list_entries(parser: &mut Parser<'_, '_>, target: &Expr<'_, '_>) {
let ExprKind::Array(elems) = &target.kind else {
return;
};
if elems
.iter()
.all(|el| matches!(el.value().kind, ExprKind::Omit))
{
parser.forbid(
ForbiddenRule::EmptyList,
"Cannot use empty list",
target.span,
);
return;
}
let is_keyed = elems
.iter()
.find(|el| !matches!(el.value().kind, ExprKind::Omit))
.is_some_and(|el| el.key.is_some());
for el in elems.iter() {
if matches!(el.value().kind, ExprKind::Omit) {
if is_keyed {
parser.forbid(
ForbiddenRule::EmptyKeyedArrayEntry,
"Cannot use empty array entries in keyed array assignment",
el.span,
);
return;
}
continue;
}
if el.is_spread() {
return;
}
if el.key.is_some() != is_keyed {
parser.forbid(
ForbiddenRule::MixedKeyedEntries,
"Cannot mix keyed and unkeyed array entries in assignments",
el.span,
);
return;
}
check_list_entries(parser, el.value());
}
}
fn is_list_syntax(source: &str, span: Span) -> bool {
source
.as_bytes()
.get(span.start as usize)
.map(|b| b.eq_ignore_ascii_case(&b'l'))
.unwrap_or(false)
}
fn check_mixed_list_short_syntax<'arena, 'src>(
parser: &mut Parser<'arena, 'src>,
lhs: &Expr<'arena, 'src>,
) {
let ExprKind::Array(_) = lhs.kind else {
return;
};
let outer_is_list = is_list_syntax(parser.source(), lhs.span);
fn walk<'arena, 'src>(
parser: &mut Parser<'arena, 'src>,
expr: &Expr<'arena, 'src>,
outer_is_list: bool,
) {
if let ExprKind::Array(elems) = &expr.kind {
if is_list_syntax(parser.source(), expr.span) != outer_is_list {
parser.forbid(
ForbiddenRule::MixedListSyntax,
"Cannot mix [] and list()",
expr.span,
);
return;
}
for el in elems.iter() {
walk(parser, el.value(), outer_is_list);
}
}
}
if let ExprKind::Array(elems) = &lhs.kind {
for el in elems.iter() {
walk(parser, el.value(), outer_is_list);
}
}
}
fn temporary_write_message(
kind: &ExprKind<'_, '_>,
version: PhpVersion,
) -> Option<(ForbiddenRule, &'static str)> {
let mut cur = kind;
let mut saw_access = false;
let mut saw_property = false;
loop {
match cur {
ExprKind::ArrayAccess(a) => {
saw_access = true;
cur = &a.array.kind;
}
ExprKind::PropertyAccess(p) => {
saw_access = true;
saw_property = true;
cur = &p.object.kind;
}
ExprKind::NullsafePropertyAccess(_) => return None,
ExprKind::Parenthesized(inner) => cur = &inner.kind,
_ => break,
}
}
if !saw_access {
return None;
}
let constant = match cur {
ExprKind::Name(_) | ExprKind::Bool(_) | ExprKind::Null => true,
ExprKind::ClassConstAccess(access) => !matches!(
&access.member.kind,
ExprKind::Identifier(m) if m.eq_ignore_ascii_case("class")
),
ExprKind::ClassConstAccessDynamic { .. } => true,
_ => false,
};
if constant && saw_property && version >= PhpVersion::Php86 {
return None;
}
match cur {
ExprKind::Clone(_) | ExprKind::CallableCreate(_) => Some((
ForbiddenRule::BuiltinResultWrite,
"Cannot use result of built-in function in write context",
)),
ExprKind::Variable(_)
| ExprKind::VariableVariable(_)
| ExprKind::StaticPropertyAccess(_)
| ExprKind::StaticPropertyAccessDynamic { .. }
| ExprKind::FunctionCall(_)
| ExprKind::MethodCall(_)
| ExprKind::NullsafeMethodCall(_)
| ExprKind::StaticMethodCall(_)
| ExprKind::StaticMethodCallDynamic(_)
| ExprKind::CloneWith(..)
| ExprKind::Error => None,
_ => Some((
ForbiddenRule::TemporaryExpressionWrite,
"Cannot use temporary expression in write context",
)),
}
}
fn parse_assign_continuation<'arena, 'src>(
parser: &mut Parser<'arena, 'src>,
lhs: Expr<'arena, 'src>,
) -> Expr<'arena, 'src> {
debug_assert!(parser.current_kind().is_assignment_op());
let span = parser.current_span();
let is_plain_assign = parser.current_kind() == TokenKind::Equals;
if matches!(
lhs.kind,
ExprKind::UnaryPrefix(UnaryPrefixExpr {
op: UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement,
..
}) | ExprKind::UnaryPostfix(UnaryPostfixExpr {
op: UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement,
..
})
) {
parser.forbid(
ForbiddenRule::IncDecAsAssignTarget,
"Cannot use increment/decrement as an assignment target.",
span,
);
} else if let Some((rule, message)) = temporary_write_message(&lhs.kind, parser.version) {
parser.forbid(rule, message, lhs.span);
} else if matches!(lhs.kind, ExprKind::Array(_)) && !is_plain_assign {
parser.forbid(
ForbiddenRule::CompoundAssignToDestructuring,
"Cannot use a destructuring pattern with a compound assignment operator",
span,
);
} else if !is_valid_assignment_target(&lhs.kind) {
parser.forbid(
ForbiddenRule::InvalidAssignTarget,
"Cannot use expression as assignment target.",
span,
);
} else if has_nullsafe_link(&lhs.kind) {
parser.forbid(
ForbiddenRule::NullsafeWrite,
"Can't use nullsafe operator in write context",
lhs.span,
);
}
if let ExprKind::Array(elems) = &lhs.kind {
parser.retract_destructuring_errors_within(lhs.span);
if is_plain_assign {
check_list_entries(parser, &lhs);
for el in elems.iter() {
if el.is_spread() {
parser.forbid(
ForbiddenRule::SpreadInDestructuring,
"Spread operator is not supported in assignments",
el.span,
);
} else if !is_valid_destructure_element(&el.value().kind, parser.version) {
parser.forbid(
ForbiddenRule::UnwritableDestructuringElement,
"Assignments can only happen to writable values",
el.value().span,
);
}
}
}
}
check_mixed_list_short_syntax(parser, &lhs);
let op_token = parser.advance();
let write_kind = if matches!(op_token.kind, TokenKind::Equals | TokenKind::CoalesceEquals) {
WriteKind::Assign
} else {
WriteKind::Modify
};
check_special_variable_write(parser, &lhs, write_kind);
let by_ref_span = parser.current_span();
let by_ref = op_token.kind == TokenKind::Equals && parser.eat(TokenKind::Ampersand).is_some();
if by_ref && matches!(lhs.kind, ExprKind::Array(_)) {
parser.forbid(
ForbiddenRule::ReferenceAssignToDestructuring,
"Cannot assign by reference to a destructuring pattern",
by_ref_span,
);
}
let op = match op_token.kind {
TokenKind::Equals => AssignOp::Assign,
TokenKind::PlusEquals => AssignOp::Plus,
TokenKind::MinusEquals => AssignOp::Minus,
TokenKind::StarEquals => AssignOp::Mul,
TokenKind::SlashEquals => AssignOp::Div,
TokenKind::PercentEquals => AssignOp::Mod,
TokenKind::StarStarEquals => AssignOp::Pow,
TokenKind::DotEquals => AssignOp::Concat,
TokenKind::AmpersandEquals => AssignOp::BitwiseAnd,
TokenKind::PipeEquals => AssignOp::BitwiseOr,
TokenKind::CaretEquals => AssignOp::BitwiseXor,
TokenKind::ShiftLeftEquals => AssignOp::ShiftLeft,
TokenKind::ShiftRightEquals => AssignOp::ShiftRight,
TokenKind::CoalesceEquals => AssignOp::Coalesce,
_ => unreachable!(
"is_assignment_op() guarantees one of the listed variants, got {:?}",
op_token.kind
),
};
let rhs = parse_expr_bp(
parser,
if by_ref {
MEMBER_ACCESS_BP
} else {
ASSIGNMENT_BP
},
);
if by_ref && matches!(rhs.kind, ExprKind::New(..)) {
parser.forbid(
ForbiddenRule::ReferenceAssignNew,
"Cannot use by-reference assignment with 'new' expression",
rhs.span,
);
} else if by_ref && has_nullsafe_link(&rhs.kind) {
parser.forbid(
ForbiddenRule::ReferenceToNullsafe,
"Cannot take reference of a nullsafe chain",
rhs.span,
);
} else if by_ref && matches!(rhs.kind, ExprKind::Parenthesized(_)) {
parser.forbid(
ForbiddenRule::ReferenceToParenthesized,
"Cannot take reference of a parenthesized expression",
rhs.span,
);
} else if by_ref && !is_variable_or_call(&rhs.kind) {
parser.forbid(
ForbiddenRule::ReferenceToNonReferenceable,
"Cannot assign reference to non referenceable value",
rhs.span,
);
} else if by_ref && matches!(rhs.kind, ExprKind::CallableCreate(_)) {
parser.forbid(
ForbiddenRule::BuiltinResultWrite,
"Cannot use result of built-in function in write context",
rhs.span,
);
} else if let Some((rule, message)) = by_ref
.then(|| temporary_write_message(&rhs.kind, parser.version))
.flatten()
{
parser.forbid(rule, message, rhs.span);
} else if by_ref {
check_special_variable_write(parser, &rhs, WriteKind::Reference);
} else if op == AssignOp::Assign && has_reference_element(&lhs.kind) {
let mut source = &rhs.kind;
while let ExprKind::Parenthesized(inner) = source {
source = &inner.kind;
}
let error = if has_nullsafe_link(source) {
Some((
ForbiddenRule::ReferenceToNullsafe,
"Cannot take reference of a nullsafe chain",
))
} else if !is_variable_or_call(source) {
Some((
ForbiddenRule::ReferenceToNonReferenceable,
"Cannot assign reference to non referenceable value",
))
} else {
None
};
if let Some((rule, message)) = error {
parser.forbid(rule, message, rhs.span);
}
}
let span = lhs.span.merge(rhs.span);
Expr {
kind: ExprKind::Assign(AssignExpr {
target: parser.alloc(lhs),
op,
value: parser.alloc(rhs),
by_ref,
}),
span,
}
}
pub fn parse_expr<'arena, 'src>(parser: &'_ mut Parser<'arena, 'src>) -> Expr<'arena, 'src> {
instrument::record_parse_expr();
parse_expr_bp(parser, 0)
}
pub fn parse_expr_bp<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
min_bp: u8,
) -> Expr<'arena, 'src> {
if min_bp != 0 {
instrument::record_parse_expr_bp_recursive();
}
if !parser.enter_nesting() {
return Expr {
kind: ExprKind::Error,
span: parser.current_span(),
};
}
let lhs = parse_atom(parser);
parse_expr_rest(parser, lhs, min_bp)
}
pub(crate) fn parse_expr_continue<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
lhs: Expr<'arena, 'src>,
min_bp: u8,
) -> Expr<'arena, 'src> {
if !parser.enter_nesting() {
return Expr {
kind: ExprKind::Error,
span: parser.current_span(),
};
}
parse_expr_rest(parser, lhs, min_bp)
}
fn parse_expr_rest<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
mut lhs: Expr<'arena, 'src>,
min_bp: u8,
) -> Expr<'arena, 'src> {
let mut chain_truth: Option<(Span, Option<bool>)> = None;
loop {
let kind = parser.current_kind();
let should_continue = match kind {
TokenKind::PlusPlus | TokenKind::MinusMinus => {
if let Some(left_bp) = precedence::postfix_binding_power(kind) {
if left_bp < min_bp {
break;
}
let op_token = parser.advance();
let op = match op_token.kind {
TokenKind::PlusPlus => UnaryPostfixOp::PostIncrement,
TokenKind::MinusMinus => UnaryPostfixOp::PostDecrement,
_ => unreachable!(
"outer arm already matched PlusPlus/MinusMinus, got {:?}",
op_token.kind
),
};
check_increment_operand(parser, &lhs);
let span = lhs.span.merge(op_token.span);
lhs = Expr {
kind: ExprKind::UnaryPostfix(UnaryPostfixExpr {
operand: parser.alloc(lhs),
op,
}),
span,
};
true
} else {
false
}
}
TokenKind::Question => {
if TERNARY_BP < min_bp {
break;
}
if parser.version >= PhpVersion::Php80 {
if let ExprKind::Ternary(lhs_tern) = &lhs.kind {
let lhs_is_short = lhs_tern.then_expr.is_none();
let incoming_is_short = parser.peek_kind() == Some(TokenKind::Colon);
if !(lhs_is_short && incoming_is_short) {
let span = parser.current_span();
parser.forbid(
ForbiddenRule::UnparenthesizedNestedTernary,
"Unparenthesized `a ? b : c ? d : e` is not supported. \
Use parentheses to make the order explicit.",
span,
);
}
}
}
parser.advance();
let then_expr = if parser.check(TokenKind::Colon) {
None
} else {
let e = parse_expr_bp(parser, 0);
Some(parser.alloc(e))
};
parser.expect(TokenKind::Colon);
let mut else_expr = parse_expr_bp(parser, TERNARY_BP + 1);
if parser.current_kind().is_assignment_op() {
else_expr = parse_assign_continuation(parser, else_expr);
}
let span = lhs.span.merge(else_expr.span);
lhs = Expr {
kind: ExprKind::Ternary(TernaryExpr {
condition: parser.alloc(lhs),
then_expr,
else_expr: parser.alloc(else_expr),
}),
span,
};
true
}
TokenKind::Arrow | TokenKind::NullsafeArrow => {
if MEMBER_ACCESS_BP < min_bp {
break;
}
check_dereferencable(parser, &lhs, kind);
let is_nullsafe = kind == TokenKind::NullsafeArrow;
if is_nullsafe {
let span = parser.current_span();
parser.require_version(PhpVersion::Php80, "nullsafe operator (?->)", span);
}
parser.advance();
let member = parse_member_name(parser);
if parser.check(TokenKind::LeftParen) {
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(lhs.span.start, parser.previous_end());
let nullsafe_lhs = nullsafe_link(&lhs.kind, true);
if (is_nullsafe && !is_this(&lhs.kind)) || nullsafe_lhs {
parser.forbid(
ForbiddenRule::NullsafeClosureCreation,
"Cannot combine nullsafe operator with Closure creation",
span,
);
}
let callable_kind = if is_nullsafe {
CallableCreateKind::NullsafeMethod {
object: parser.alloc(lhs),
method: parser.alloc(member),
}
} else {
CallableCreateKind::Method {
object: parser.alloc(lhs),
method: parser.alloc(member),
}
};
lhs = Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: callable_kind,
}),
span,
};
}
ArgListResult::Args(args) => {
let span = Span::new(lhs.span.start, parser.previous_end());
let call = parser.alloc(MethodCallExpr {
object: parser.alloc(lhs),
method: parser.alloc(member),
args,
});
let expr_kind = if is_nullsafe {
ExprKind::NullsafeMethodCall(call)
} else {
ExprKind::MethodCall(call)
};
lhs = Expr {
kind: expr_kind,
span,
};
}
}
} else {
let span = Span::new(lhs.span.start, member.span.end);
let expr_kind = if is_nullsafe {
ExprKind::NullsafePropertyAccess(PropertyAccessExpr {
object: parser.alloc(lhs),
property: parser.alloc(member),
})
} else {
ExprKind::PropertyAccess(PropertyAccessExpr {
object: parser.alloc(lhs),
property: parser.alloc(member),
})
};
lhs = Expr {
kind: expr_kind,
span,
};
}
true
}
TokenKind::QuestionQuestion => {
if NULL_COALESCE_LEFT_BP < min_bp {
break;
}
parser.advance();
let mut rhs = parse_expr_bp(parser, TERNARY_BP + 1);
if parser.current_kind().is_assignment_op() {
rhs = parse_assign_continuation(parser, rhs);
}
let span = lhs.span.merge(rhs.span);
lhs = Expr {
kind: ExprKind::NullCoalesce(NullCoalesceExpr {
left: parser.alloc(lhs),
right: parser.alloc(rhs),
}),
span,
};
true
}
_ if kind.is_assignment_op() => {
if ASSIGNMENT_BP < min_bp {
break;
}
lhs = parse_assign_continuation(parser, lhs);
true
}
_ => false,
};
if should_continue {
continue;
}
if kind == TokenKind::DoubleColon {
if SCOPE_RESOLUTION_BP < min_bp {
break;
}
check_dereferencable(parser, &lhs, kind);
parser.advance(); if matches!(lhs.kind, ExprKind::Parenthesized(_) | ExprKind::Array(_)) {
if parser.check(TokenKind::Class) {
check_class_name_fetch_literal(parser, &lhs);
} else {
check_class_name_literal(parser, &lhs);
}
}
if parser.check(TokenKind::Variable) {
let token = parser.advance();
let var_name = parser.variable_name(token);
let var_span = token.span;
if parser.check(TokenKind::LeftParen) {
let method = parser.alloc(Expr {
kind: ExprKind::Variable(NameStr::from_source(var_name)),
span: var_span,
});
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::StaticMethod {
class: parser.alloc(lhs),
method,
},
}),
span,
};
}
ArgListResult::Args(args) => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::StaticMethodCallDynamic(parser.alloc(
StaticMethodCallDynamicExpr {
class: parser.alloc(lhs),
method,
args,
},
)),
span,
};
}
}
} else {
let member = parser.alloc(Expr {
kind: ExprKind::Identifier(NameStr::from_source(var_name)),
span: var_span,
});
let span = Span::new(lhs.span.start, var_span.end);
lhs = Expr {
kind: ExprKind::StaticPropertyAccess(StaticAccessExpr {
class: parser.alloc(lhs),
member,
}),
span,
};
}
} else if parser.check(TokenKind::Dollar) {
let member = atom::parse_atom_nested(parser);
let span = Span::new(lhs.span.start, member.span.end);
lhs = Expr {
kind: ExprKind::StaticPropertyAccessDynamic {
class: parser.alloc(lhs),
member: parser.alloc(member),
},
span,
};
} else if parser.check(TokenKind::LeftBrace) {
let brace_span = parser.current_span();
parser.advance(); let member = parse_expr(parser);
parser.expect(TokenKind::RightBrace);
if parser.check(TokenKind::LeftParen) {
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::StaticMethod {
class: parser.alloc(lhs),
method: parser.alloc(member),
},
}),
span,
};
}
ArgListResult::Args(args) => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::StaticMethodCallDynamic(parser.alloc(
StaticMethodCallDynamicExpr {
class: parser.alloc(lhs),
method: parser.alloc(member),
args,
},
)),
span,
};
}
}
} else {
parser.require_version(
PhpVersion::Php83,
"dynamic class constant fetch",
brace_span,
);
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ClassConstAccessDynamic {
class: parser.alloc(lhs),
member: parser.alloc(member),
},
span,
};
}
} else if parser.check(TokenKind::Class) {
if parser.version < PhpVersion::Php80 && !matches!(lhs.kind, ExprKind::Name(_)) {
parser.forbid(
ForbiddenRule::DynamicClassFetch,
"Cannot use ::class with dynamic class name",
lhs.span,
);
}
let token = parser.advance();
let span = Span::new(lhs.span.start, token.span.end);
lhs = Expr {
kind: ExprKind::ClassConstAccess(StaticAccessExpr {
class: parser.alloc(lhs),
member: parser.alloc(Expr {
kind: ExprKind::Identifier(NameStr::from_source("class")),
span: token.span,
}),
}),
span,
};
} else {
let (member_name, member_span) =
if let Some(result) = parser.eat_identifier_or_keyword() {
result
} else {
let err_span = parser.current_span();
parser.error(ParseError::Expected {
expected: "identifier".into(),
found: parser.current_kind().description().into(),
span: err_span,
});
let span = Span::new(lhs.span.start, err_span.end);
lhs = Expr {
kind: ExprKind::Error,
span,
};
continue;
};
if parser.check(TokenKind::LeftParen) {
let method = parser.alloc(Expr {
kind: ExprKind::Identifier(NameStr::from_source(member_name)),
span: member_span,
});
match parse_arg_list_or_callable(parser) {
ArgListResult::CallableMarker => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::CallableCreate(CallableCreateExpr {
kind: CallableCreateKind::StaticMethod {
class: parser.alloc(lhs),
method,
},
}),
span,
};
}
ArgListResult::Args(args) => {
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::StaticMethodCall(parser.alloc(
StaticMethodCallExpr {
class: parser.alloc(lhs),
method,
args,
},
)),
span,
};
}
}
} else {
let member = parser.alloc(Expr {
kind: ExprKind::Identifier(NameStr::from_source(member_name)),
span: member_span,
});
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ClassConstAccess(StaticAccessExpr {
class: parser.alloc(lhs),
member,
}),
span,
};
}
}
continue;
}
if kind == TokenKind::LeftBracket {
if MEMBER_ACCESS_BP < min_bp {
break;
}
check_dereferencable(parser, &lhs, kind);
parser.advance(); let index = if parser.check(TokenKind::RightBracket) {
parser.empty_dim_count += 1;
None
} else {
let e = parse_expr(parser);
Some(parser.alloc(e))
};
parser.expect(TokenKind::RightBracket);
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ArrayAccess(ArrayAccessExpr {
array: parser.alloc(lhs),
index,
}),
span,
};
continue;
}
if kind == TokenKind::LeftBrace {
if MEMBER_ACCESS_BP < min_bp || parser.no_brace_subscript {
break;
}
let brace_span = parser.current_span();
parser.advance(); let index = if parser.check(TokenKind::RightBrace) {
None
} else {
let e = parse_expr(parser);
Some(parser.alloc(e))
};
parser.expect(TokenKind::RightBrace);
if parser.version >= PhpVersion::Php80 {
parser.forbid(
ForbiddenRule::CurlyOffsetAccess,
CURLY_OFFSET_REMOVED,
brace_span,
);
} else if matches!(
lhs.kind,
ExprKind::Name(_)
| ExprKind::ClassConstAccess(_)
| ExprKind::MagicConst(_)
| ExprKind::InterpolatedString(_)
) {
parser.error(ParseError::Expected {
expected: "';'".into(),
found: "'{'".into(),
span: brace_span,
});
}
let span = Span::new(lhs.span.start, parser.previous_end());
lhs = Expr {
kind: ExprKind::ArrayAccess(ArrayAccessExpr {
array: parser.alloc(lhs),
index,
}),
span,
};
continue;
}
if kind == TokenKind::LeftParen {
if MEMBER_ACCESS_BP < min_bp {
break;
}
check_dereferencable(parser, &lhs, kind);
lhs = parse_function_call(parser, lhs);
continue;
}
if let Some((left_bp, right_bp)) = precedence::infix_binding_power(kind) {
if left_bp < min_bp {
break;
}
if let Some(current_level) = nonassoc_chain_level_for_token(kind) {
if matches!(&lhs.kind, ExprKind::Binary(b) if nonassoc_chain_level_for_op(b.op) == Some(current_level))
{
let span = parser.current_span();
parser.forbid(
ForbiddenRule::NonAssociativeChain,
"Chaining non-associative operators requires explicit parentheses.",
span,
);
}
}
let op_token = parser.advance();
if op_token.kind == TokenKind::PipeArrow {
parser.require_version(PhpVersion::Php85, "pipe operator (|>)", op_token.span);
}
let op = token_to_binary_op(op_token.kind).unwrap_or_else(|| {
unreachable!(
"infix_binding_power returned Some for {:?} but token_to_binary_op returned None",
op_token.kind
)
});
let lhs_truth = match chain_truth {
_ if !is_short_circuit(op) => None,
Some((span, truth)) if span == lhs.span => truth,
_ => folded_truth(&lhs),
};
let short_circuits = match (op, lhs_truth) {
(BinaryOp::BooleanAnd | BinaryOp::LogicalAnd, Some(truth)) => !truth,
(BinaryOp::BooleanOr | BinaryOp::LogicalOr, Some(truth)) => truth,
_ => false,
};
if op == BinaryOp::Instanceof && parser.check(TokenKind::LeftParen) {
let span = parser.current_span();
parser.require_version(PhpVersion::Php80, "instanceof with an expression", span);
}
let instanceof_static = op == BinaryOp::Instanceof
&& parser.check(TokenKind::Static)
&& parser.peek_kind() != Some(TokenKind::DoubleColon);
let mut rhs = if instanceof_static {
let token = parser.advance();
parser.has_class_keyword_ref = true;
atom::simple_name_expr("static", token.span)
} else {
parse_expr_bp(parser, right_bp)
};
if parser.current_kind().is_assignment_op() {
rhs = parse_assign_continuation(parser, rhs);
}
if short_circuits {
parser.uncompiled_spans.push(rhs.span);
}
if op == BinaryOp::Instanceof {
if is_class_name_reference(&rhs) {
check_class_name_literal(parser, &rhs);
} else {
parser.forbid(ForbiddenRule::InvalidInstanceofOperand, "right side of instanceof must be a class name, a variable or a parenthesized expression", rhs.span);
}
}
if op == BinaryOp::Pipe && matches!(rhs.kind, ExprKind::ArrowFunction(_)) {
parser.forbid(
ForbiddenRule::UnparenthesizedArrowInPipe,
"arrow function on the right side of pipe operator must be parenthesized",
rhs.span,
);
}
let span = lhs.span.merge(rhs.span);
if is_short_circuit(op) {
let truth = match lhs_truth {
Some(truth) if short_circuits => Some(truth),
Some(_) => folded_truth(&rhs),
None => None,
};
chain_truth = Some((span, truth));
}
lhs = Expr {
kind: ExprKind::Binary(BinaryExpr {
left: parser.alloc(lhs),
op,
right: parser.alloc(rhs),
}),
span,
};
continue;
}
break;
}
parser.leave_nesting();
lhs
}
pub(crate) fn parse_member_name<'arena, 'src>(
parser: &'_ mut Parser<'arena, 'src>,
) -> Expr<'arena, 'src> {
if parser.check(TokenKind::Identifier)
|| parser.is_semi_reserved_keyword()
|| parser.check(TokenKind::HaltCompiler)
{
let token = parser.advance();
let text = &parser.source[token.span.start as usize..token.span.end as usize];
return Expr {
kind: ExprKind::Identifier(NameStr::from_source(text)),
span: token.span,
};
}
match parser.current_kind() {
TokenKind::Variable => {
let token = parser.advance();
let name = parser.variable_name(token);
Expr {
kind: ExprKind::Variable(NameStr::from_source(name)),
span: token.span,
}
}
TokenKind::LeftBrace => {
parser.advance();
let inner = parse_expr(parser);
parser.expect(TokenKind::RightBrace);
inner
}
TokenKind::Dollar => {
let start = parser.start_span();
parser.advance(); let inner = if parser.eat(TokenKind::LeftBrace).is_some() {
let e = parse_expr(parser);
parser.expect(TokenKind::RightBrace);
e
} else {
atom::parse_variable_variable_name(parser)
};
let span = Span::new(start, parser.previous_end());
Expr {
kind: ExprKind::VariableVariable(parser.alloc(inner)),
span,
}
}
_ => {
let span = parser.current_span();
parser.error(ParseError::Expected {
expected: "member name".into(),
found: parser.current_kind().description().into(),
span,
});
Expr {
kind: ExprKind::Error,
span,
}
}
}
}
fn token_to_binary_op(kind: TokenKind) -> Option<BinaryOp> {
match kind {
TokenKind::Plus => Some(BinaryOp::Add),
TokenKind::Minus => Some(BinaryOp::Sub),
TokenKind::Star => Some(BinaryOp::Mul),
TokenKind::Slash => Some(BinaryOp::Div),
TokenKind::Percent => Some(BinaryOp::Mod),
TokenKind::StarStar => Some(BinaryOp::Pow),
TokenKind::Dot => Some(BinaryOp::Concat),
TokenKind::EqualsEquals => Some(BinaryOp::Equal),
TokenKind::BangEquals => Some(BinaryOp::NotEqual),
TokenKind::EqualsEqualsEquals => Some(BinaryOp::Identical),
TokenKind::BangEqualsEquals => Some(BinaryOp::NotIdentical),
TokenKind::LessThan => Some(BinaryOp::Less),
TokenKind::GreaterThan => Some(BinaryOp::Greater),
TokenKind::LessThanEquals => Some(BinaryOp::LessOrEqual),
TokenKind::GreaterThanEquals => Some(BinaryOp::GreaterOrEqual),
TokenKind::Spaceship => Some(BinaryOp::Spaceship),
TokenKind::AmpersandAmpersand => Some(BinaryOp::BooleanAnd),
TokenKind::PipePipe => Some(BinaryOp::BooleanOr),
TokenKind::Ampersand => Some(BinaryOp::BitwiseAnd),
TokenKind::Pipe => Some(BinaryOp::BitwiseOr),
TokenKind::Caret => Some(BinaryOp::BitwiseXor),
TokenKind::ShiftLeft => Some(BinaryOp::ShiftLeft),
TokenKind::ShiftRight => Some(BinaryOp::ShiftRight),
TokenKind::And => Some(BinaryOp::LogicalAnd),
TokenKind::Or => Some(BinaryOp::LogicalOr),
TokenKind::Xor => Some(BinaryOp::LogicalXor),
TokenKind::Instanceof => Some(BinaryOp::Instanceof),
TokenKind::PipeArrow => Some(BinaryOp::Pipe),
_ => None,
}
}