use std::fmt;
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum ExpressionErrorKind {
#[error("Undefined variable: '{name}'.{suggestion}")]
UndefinedVariable { name: String, suggestion: String },
#[error("Unknown function: '{name}'")]
UnknownFunction { name: String },
#[error("{message}")]
TypeError { message: String },
#[error("Integer overflow: result is outside the 64-bit signed range")]
IntegerOverflow,
#[error("{op} by zero")]
DivisionByZero { op: &'static str },
#[error("{message}")]
FloatError { message: String },
#[error("{message}")]
IndexOutOfBounds { message: String },
#[error("Expression memory usage ({used} bytes) exceeded limit ({limit} bytes)")]
MemoryLimitExceeded { used: usize, limit: usize },
#[error("Expression operation count ({count}) exceeded limit ({limit})")]
OperationLimitExceeded { count: usize, limit: usize },
#[error("Expression nesting depth ({depth}) exceeded limit ({limit})")]
ExpressionTooDeep { depth: usize, limit: usize },
#[error("{feature}")]
UnsupportedSyntax { feature: String },
#[error("{0}")]
ExplicitFail(String),
#[error("{0}")]
ParseError(String),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone)]
pub struct ExpressionError {
inner: Box<ExpressionErrorInner>,
}
#[derive(Debug, Clone)]
struct ExpressionErrorInner {
kind: ExpressionErrorKind,
expr: Option<String>,
col_offset: Option<usize>,
end_col_offset: Option<usize>,
caret_offset: Option<usize>,
sub_errors: Option<Vec<ExpressionError>>,
}
impl ExpressionError {
pub fn from_kind(kind: ExpressionErrorKind) -> Self {
Self {
inner: Box::new(ExpressionErrorInner {
kind,
expr: None,
col_offset: None,
end_col_offset: None,
caret_offset: None,
sub_errors: None,
}),
}
}
pub fn new(message: impl Into<String>) -> Self {
Self::from_kind(ExpressionErrorKind::Other(message.into()))
}
pub fn integer_overflow() -> Self {
Self::from_kind(ExpressionErrorKind::IntegerOverflow)
}
pub fn division_by_zero(op: &'static str) -> Self {
Self::from_kind(ExpressionErrorKind::DivisionByZero { op })
}
pub fn float_error(message: impl Into<String>) -> Self {
Self::from_kind(ExpressionErrorKind::FloatError {
message: message.into(),
})
}
pub fn type_error(message: impl Into<String>) -> Self {
Self::from_kind(ExpressionErrorKind::TypeError {
message: message.into(),
})
}
pub fn index_out_of_bounds(message: impl Into<String>) -> Self {
Self::from_kind(ExpressionErrorKind::IndexOutOfBounds {
message: message.into(),
})
}
pub fn unsupported(feature: impl Into<String>) -> Self {
Self::from_kind(ExpressionErrorKind::UnsupportedSyntax {
feature: feature.into(),
})
}
pub fn explicit_fail(message: impl Into<String>) -> Self {
Self::from_kind(ExpressionErrorKind::ExplicitFail(message.into()))
}
pub fn parse_error(message: impl Into<String>) -> Self {
Self::from_kind(ExpressionErrorKind::ParseError(message.into()))
}
pub fn expression_too_deep(depth: usize, limit: usize) -> Self {
Self::from_kind(ExpressionErrorKind::ExpressionTooDeep { depth, limit })
}
pub fn kind(&self) -> &ExpressionErrorKind {
&self.inner.kind
}
pub fn message(&self) -> String {
self.inner.kind.to_string()
}
pub fn sub_errors(&self) -> &[ExpressionError] {
match &self.inner.sub_errors {
Some(v) => v.as_slice(),
None => &[],
}
}
pub fn with_sub_errors(mut self, sub_errors: Vec<ExpressionError>) -> Self {
if !sub_errors.is_empty() {
self.inner.sub_errors = Some(sub_errors);
}
self
}
pub fn expr(&self) -> Option<&str> {
self.inner.expr.as_deref()
}
pub fn col_offset(&self) -> Option<usize> {
self.inner.col_offset
}
pub fn end_col_offset(&self) -> Option<usize> {
self.inner.end_col_offset
}
pub fn caret_offset(&self) -> Option<usize> {
self.inner.caret_offset
}
#[must_use]
pub fn with_node(mut self, expr_source: &str, node: &ruff_python_ast::Expr) -> Self {
use ruff_text_size::Ranged;
if self.inner.expr.is_some() {
return self;
}
self.inner.expr = Some(expr_source.to_string());
let range = node.range();
self.inner.col_offset = Some(range.start().to_usize());
self.inner.end_col_offset = Some(range.end().to_usize());
self.inner.caret_offset = Some(compute_caret_offset(expr_source, node));
self
}
#[must_use]
pub fn with_span(mut self, expr_source: &str, col: usize, end_col: usize) -> Self {
if self.inner.expr.is_some() {
return self;
}
self.inner.expr = Some(expr_source.to_string());
self.inner.col_offset = Some(col);
self.inner.end_col_offset = Some(end_col);
self
}
pub fn set_source_span(
&mut self,
expr_source: &str,
col: usize,
end_col: usize,
caret_offset: usize,
) {
self.inner.expr = Some(expr_source.to_string());
self.inner.col_offset = Some(col);
self.inner.end_col_offset = Some(end_col);
self.inner.caret_offset = Some(caret_offset);
}
pub fn message_with_expr_prefix(&self, prefix: &str) -> String {
let (Some(expr), Some(col), Some(end_col)) = (
&self.inner.expr,
self.inner.col_offset,
self.inner.end_col_offset,
) else {
return self.to_string();
};
if expr.contains('\n') {
return self.to_string();
}
let msg = self.message();
let mut out = msg;
out.push_str("\n ");
out.push_str(prefix);
out.push_str(expr);
out.push_str("\n ");
let _ = write_caret_line(
&mut out,
col + prefix.len(),
end_col + prefix.len(),
self.inner.caret_offset.unwrap_or(0),
);
out
}
}
impl fmt::Display for ExpressionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner.kind)?;
if let (Some(expr), Some(col), Some(end_col)) = (
&self.inner.expr,
self.inner.col_offset,
self.inner.end_col_offset,
) {
let is_multiline = expr.contains('\n');
let (col, end_col) = if is_multiline {
(col.saturating_sub(1), end_col.saturating_sub(1))
} else {
(col, end_col)
};
let (expr_line, line_col, line_end_col) = if is_multiline {
let mut pos = 0;
let mut found_line = expr.as_str();
let mut line_start = 0;
for line in expr.split('\n') {
if pos + line.len() >= col {
found_line = line;
line_start = pos;
break;
}
pos += line.len() + 1; }
let lc = col - line_start;
let lec = if end_col > line_start {
(end_col - line_start).min(found_line.len())
} else {
lc + 1
};
(found_line, lc, lec)
} else {
(expr.as_str(), col, end_col)
};
write!(f, "\n {expr_line}\n ")?;
write_caret_line(
f,
line_col,
line_end_col,
self.inner.caret_offset.unwrap_or(0),
)?;
}
Ok(())
}
}
impl std::error::Error for ExpressionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.inner.kind)
}
}
pub(crate) fn write_caret_line(
w: &mut dyn std::fmt::Write,
col: usize,
end_col: usize,
caret_offset: usize,
) -> std::fmt::Result {
let span_len = end_col.saturating_sub(col);
for _ in 0..col {
w.write_char(' ')?;
}
if span_len > 1 {
let caret_idx = caret_offset.min(span_len.saturating_sub(1));
for _ in 0..caret_idx {
w.write_char('~')?;
}
w.write_char('^')?;
for _ in 0..span_len.saturating_sub(caret_idx + 1) {
w.write_char('~')?;
}
} else {
w.write_char('^')?;
}
Ok(())
}
fn compute_caret_offset(expr: &str, node: &ruff_python_ast::Expr) -> usize {
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
match node {
ast::Expr::BinOp(b) => {
let left_end = b.left.range().end().to_usize();
let right_start = b.right.range().start().to_usize();
let node_start = node.range().start().to_usize();
let bytes = expr.as_bytes();
let mut i = right_start.saturating_sub(1);
while i > left_end
&& i < bytes.len()
&& (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'(')
{
i -= 1;
}
if i > left_end
&& i < bytes.len()
&& i >= 1
&& (bytes[i - 1..=i] == *b"**" || bytes[i - 1..=i] == *b"//")
{
return (i - 1) - node_start;
}
if i >= left_end && i < bytes.len() {
i - node_start
} else {
0
}
}
ast::Expr::Attribute(a) => {
let value_end = a.value.range().end().to_usize();
let node_start = node.range().start().to_usize();
(value_end + 1).saturating_sub(node_start) }
ast::Expr::Call(c) => {
if let ast::Expr::Attribute(a) = &*c.func {
let value_end = a.value.range().end().to_usize();
let node_start = node.range().start().to_usize();
(value_end + 1).saturating_sub(node_start)
} else {
0
}
}
ast::Expr::Subscript(s) => {
let value_end = s.value.range().end().to_usize();
let node_start = node.range().start().to_usize();
value_end.saturating_sub(node_start)
}
_ => 0,
}
}