#[cfg(test)]
mod tests;
use crate::engine::errors::ErrorType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExceptionClass {
Throwable,
Exception,
Error,
RuntimeException,
LogicException,
InvalidArgumentException,
TypeError,
ValueError,
DivisionByZeroError,
OverflowException,
UnderflowException,
OutOfRangeException,
OutOfBoundsException,
BadMethodCallException,
BadFunctionCallException,
Custom(String),
}
impl ExceptionClass {
pub fn is_subclass_of(&self, parent: &ExceptionClass) -> bool {
match parent {
ExceptionClass::Throwable => true, ExceptionClass::Exception => matches!(
self,
ExceptionClass::Exception
| ExceptionClass::RuntimeException
| ExceptionClass::LogicException
| ExceptionClass::InvalidArgumentException
| ExceptionClass::OverflowException
| ExceptionClass::UnderflowException
| ExceptionClass::OutOfRangeException
| ExceptionClass::OutOfBoundsException
| ExceptionClass::BadMethodCallException
| ExceptionClass::BadFunctionCallException
| ExceptionClass::Custom(_)
),
ExceptionClass::Error => matches!(
self,
ExceptionClass::Error
| ExceptionClass::TypeError
| ExceptionClass::ValueError
| ExceptionClass::DivisionByZeroError
),
ExceptionClass::RuntimeException => matches!(
self,
ExceptionClass::RuntimeException
| ExceptionClass::OverflowException
| ExceptionClass::UnderflowException
| ExceptionClass::OutOfRangeException
),
ExceptionClass::LogicException => matches!(
self,
ExceptionClass::LogicException
| ExceptionClass::InvalidArgumentException
| ExceptionClass::OutOfBoundsException
| ExceptionClass::BadMethodCallException
| ExceptionClass::BadFunctionCallException
),
_ => self == parent,
}
}
pub fn name(&self) -> &str {
match self {
ExceptionClass::Throwable => "Throwable",
ExceptionClass::Exception => "Exception",
ExceptionClass::Error => "Error",
ExceptionClass::RuntimeException => "RuntimeException",
ExceptionClass::LogicException => "LogicException",
ExceptionClass::InvalidArgumentException => "InvalidArgumentException",
ExceptionClass::TypeError => "TypeError",
ExceptionClass::ValueError => "ValueError",
ExceptionClass::DivisionByZeroError => "DivisionByZeroError",
ExceptionClass::OverflowException => "OverflowException",
ExceptionClass::UnderflowException => "UnderflowException",
ExceptionClass::OutOfRangeException => "OutOfRangeException",
ExceptionClass::OutOfBoundsException => "OutOfBoundsException",
ExceptionClass::BadMethodCallException => "BadMethodCallException",
ExceptionClass::BadFunctionCallException => "BadFunctionCallException",
ExceptionClass::Custom(name) => name.as_str(),
}
}
}
#[derive(Debug, Clone)]
pub struct PhpException {
pub class: ExceptionClass,
pub message: String,
pub code: i64,
pub file: Option<String>,
pub line: u32,
pub previous: Option<Box<PhpException>>,
pub trace: Vec<StackFrame>,
}
#[derive(Debug, Clone)]
pub struct StackFrame {
pub file: Option<String>,
pub line: u32,
pub function: Option<String>,
pub class: Option<String>,
pub args: Vec<String>,
}
impl PhpException {
pub fn new(class: ExceptionClass, message: &str) -> Self {
Self {
class,
message: message.to_string(),
code: 0,
file: None,
line: 0,
previous: None,
trace: Vec::new(),
}
}
pub fn with_code(class: ExceptionClass, message: &str, code: i64) -> Self {
Self {
class,
message: message.to_string(),
code,
file: None,
line: 0,
previous: None,
trace: Vec::new(),
}
}
pub fn with_previous(
class: ExceptionClass,
message: &str,
code: i64,
previous: PhpException,
) -> Self {
Self {
class,
message: message.to_string(),
code,
file: None,
line: 0,
previous: Some(Box::new(previous)),
trace: Vec::new(),
}
}
pub fn set_location(&mut self, file: &str, line: u32) {
self.file = Some(file.to_string());
self.line = line;
}
pub fn add_trace_frame(&mut self, frame: StackFrame) {
self.trace.push(frame);
}
pub fn get_message(&self) -> &str {
&self.message
}
pub fn get_code(&self) -> i64 {
self.code
}
pub fn get_previous(&self) -> Option<&PhpException> {
self.previous.as_deref()
}
pub fn to_string_repr(&self) -> String {
let mut result = format!(
"{}: {} in {}:{}",
self.class.name(),
self.message,
self.file.as_deref().unwrap_or("unknown"),
self.line
);
if !self.trace.is_empty() {
result.push_str("\nStack trace:");
for (i, frame) in self.trace.iter().enumerate() {
result.push_str(&format!(
"\n#{} {}({}): {}{}",
i,
frame.file.as_deref().unwrap_or("unknown"),
frame.line,
if let Some(ref class) = frame.class {
format!("{}::", class)
} else {
String::new()
},
frame.function.as_deref().unwrap_or("{main}"),
));
}
}
if let Some(ref prev) = self.previous {
result.push_str(&format!("\n\nCaused by:\n{}", prev.to_string_repr()));
}
result
}
}
#[derive(Debug, Clone)]
pub struct CatchBlock {
pub exception_class: ExceptionClass,
pub variable_name: String,
pub body_start: usize,
pub body_end: usize,
}
#[derive(Debug, Clone)]
pub struct TryCatchBlock {
pub try_start: usize,
pub try_end: usize,
pub catches: Vec<CatchBlock>,
pub finally_start: Option<usize>,
pub finally_end: Option<usize>,
}
impl TryCatchBlock {
pub fn new(try_start: usize) -> Self {
Self {
try_start,
try_end: 0,
catches: Vec::new(),
finally_start: None,
finally_end: None,
}
}
pub fn find_catch(&self, exception: &PhpException) -> Option<&CatchBlock> {
self.catches
.iter()
.find(|c| exception.class.is_subclass_of(&c.exception_class))
}
}
pub struct ExceptionState {
try_catch_stack: Vec<TryCatchBlock>,
current_exception: Option<PhpException>,
}
impl ExceptionState {
pub fn new() -> Self {
Self {
try_catch_stack: Vec::new(),
current_exception: None,
}
}
pub fn push_try_catch(&mut self, block: TryCatchBlock) {
self.try_catch_stack.push(block);
}
pub fn pop_try_catch(&mut self) -> Option<TryCatchBlock> {
self.try_catch_stack.pop()
}
pub fn current_try_catch(&self) -> Option<&TryCatchBlock> {
self.try_catch_stack.last()
}
pub fn throw(&mut self, exception: PhpException) -> ExceptionAction {
for block in self.try_catch_stack.iter().rev() {
if let Some(catch) = block.find_catch(&exception) {
let action = ExceptionAction::Catch {
variable_name: catch.variable_name.clone(),
jump_to: catch.body_start,
exception: exception.clone(),
};
return action;
}
}
self.current_exception = Some(exception);
ExceptionAction::Uncaught
}
pub fn get_current_exception(&self) -> Option<&PhpException> {
self.current_exception.as_ref()
}
pub fn clear_exception(&mut self) {
self.current_exception = None;
}
pub fn has_exception(&self) -> bool {
self.current_exception.is_some()
}
pub fn depth(&self) -> usize {
self.try_catch_stack.len()
}
}
impl Default for ExceptionState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub enum ExceptionAction {
Catch {
variable_name: String,
jump_to: usize,
exception: PhpException,
},
Uncaught,
}
pub fn error_to_exception(
error_type: ErrorType,
message: &str,
file: Option<&str>,
line: u32,
) -> PhpException {
let class = match error_type {
ErrorType::Error | ErrorType::CoreError | ErrorType::CompileError => {
ExceptionClass::Error
}
ErrorType::Warning
| ErrorType::CoreWarning
| ErrorType::CompileWarning
| ErrorType::UserWarning => ExceptionClass::RuntimeException,
ErrorType::RecoverableError => ExceptionClass::Error,
_ => ExceptionClass::Exception,
};
let mut exception = PhpException::new(class, message);
if let Some(f) = file {
exception.set_location(f, line);
}
exception
}