Skip to main content

python_ast/ast/tree/
raise_stmt.rs

1use proc_macro2::TokenStream;
2use pyo3::{Borrowed, FromPyObject, PyAny, PyResult, prelude::PyAnyMethods};
3use quote::quote;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    CodeGen, CodeGenContext, ExprType, Node, PythonOptions, SymbolTableScopes,
8};
9
10/// Raise statement (raise [exception [from cause]])
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
12pub struct Raise {
13    /// The exception to raise (optional - bare raise re-raises current exception)
14    pub exc: Option<ExprType>,
15    /// The cause of the exception (optional - used with 'from' clause)
16    pub cause: Option<ExprType>,
17    /// Position information
18    pub lineno: Option<usize>,
19    pub col_offset: Option<usize>,
20    pub end_lineno: Option<usize>,
21    pub end_col_offset: Option<usize>,
22}
23
24impl<'a, 'py> FromPyObject<'a, 'py> for Raise {
25    type Error = pyo3::PyErr;
26    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
27        // Extract exc (optional)
28        let exc: Option<ExprType> = if let Ok(exc_attr) = ob.getattr("exc") {
29            if exc_attr.is_none() {
30                None
31            } else {
32                Some(exc_attr.extract()?)
33            }
34        } else {
35            None
36        };
37        
38        // Extract cause (optional)
39        let cause: Option<ExprType> = if let Ok(cause_attr) = ob.getattr("cause") {
40            if cause_attr.is_none() {
41                None
42            } else {
43                Some(cause_attr.extract()?)
44            }
45        } else {
46            None
47        };
48        
49        Ok(Raise {
50            exc,
51            cause,
52            lineno: ob.lineno(),
53            col_offset: ob.col_offset(),
54            end_lineno: ob.end_lineno(),
55            end_col_offset: ob.end_col_offset(),
56        })
57    }
58}
59
60impl Node for Raise {
61    fn lineno(&self) -> Option<usize> { self.lineno }
62    fn col_offset(&self) -> Option<usize> { self.col_offset }
63    fn end_lineno(&self) -> Option<usize> { self.end_lineno }
64    fn end_col_offset(&self) -> Option<usize> { self.end_col_offset }
65}
66
67impl CodeGen for Raise {
68    type Context = CodeGenContext;
69    type Options = PythonOptions;
70    type SymbolTable = SymbolTableScopes;
71
72    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
73        let symbols = if let Some(exc) = self.exc {
74            exc.find_symbols(symbols)
75        } else {
76            symbols
77        };
78        
79        if let Some(cause) = self.cause {
80            cause.find_symbols(symbols)
81        } else {
82            symbols
83        }
84    }
85
86    fn to_rust(
87        self,
88        ctx: Self::Context,
89        options: Self::Options,
90        symbols: Self::SymbolTable,
91    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
92        let exc_tokens = match self.exc {
93            Some(exc) => {
94                let mut tokens =
95                    exception_value(&exc, ctx.clone(), options.clone(), symbols.clone())?;
96                if let Some(cause) = self.cause {
97                    // `raise X from Y`: keep the cause visible in the message
98                    // rather than dropping it.
99                    let cause_tokens = cause.to_rust(ctx.clone(), options, symbols)?;
100                    tokens = quote! {
101                        {
102                            let mut __rython_raised = #tokens;
103                            __rython_raised.message =
104                                format!("{} [from {}]", __rython_raised.message, #cause_tokens);
105                            __rython_raised
106                        }
107                    };
108                }
109                tokens
110            }
111            None => {
112                // Bare `raise` re-raises the exception the enclosing except
113                // handler caught (a runtime error outside a handler, as in
114                // Python).
115                if !ctx.in_except_handler() {
116                    return Err(
117                        "bare `raise` outside an except handler has no exception to re-raise"
118                            .to_string()
119                            .into(),
120                    );
121                }
122                quote!(__rython_exc.clone())
123            }
124        };
125
126        // Functions return Result<T, PyException>, so raising is returning
127        // Err: inside a try block it returns out of the block's Result
128        // closure to be caught by the handlers, and anywhere else it
129        // propagates out of the function, as in Python.
130        Ok(quote!(return Err(#exc_tokens)))
131    }
132}
133
134/// Names that look like Python exception classes, so `raise Name` /
135/// `raise Name(...)` can construct a PyException carrying that class name.
136/// Anything else is treated as an expression already producing a
137/// PyException value (e.g. a variable bound by `except ... as e`).
138fn is_exception_class_name(name: &str) -> bool {
139    matches!(
140        name,
141        "Exception"
142            | "BaseException"
143            | "ArithmeticError"
144            | "AssertionError"
145            | "AttributeError"
146            | "BufferError"
147            | "EOFError"
148            | "FileExistsError"
149            | "FileNotFoundError"
150            | "FloatingPointError"
151            | "ImportError"
152            | "IndentationError"
153            | "IndexError"
154            | "InterruptedError"
155            | "IsADirectoryError"
156            | "KeyError"
157            | "KeyboardInterrupt"
158            | "LookupError"
159            | "MemoryError"
160            | "ModuleNotFoundError"
161            | "NameError"
162            | "NotADirectoryError"
163            | "NotImplementedError"
164            | "OSError"
165            | "OverflowError"
166            | "PermissionError"
167            | "RecursionError"
168            | "ReferenceError"
169            | "RuntimeError"
170            | "StopAsyncIteration"
171            | "StopIteration"
172            | "SyntaxError"
173            | "SystemError"
174            | "SystemExit"
175            | "TabError"
176            | "TimeoutError"
177            | "TypeError"
178            | "UnboundLocalError"
179            | "UnicodeDecodeError"
180            | "UnicodeEncodeError"
181            | "UnicodeError"
182            | "ValueError"
183            | "ZeroDivisionError"
184    ) || name.ends_with("Error")
185        || name.ends_with("Exception")
186        || name.ends_with("Warning")
187}
188
189/// Lower the raised expression to a PyException value: `Name(...)` and bare
190/// `Name` forms that look like exception classes construct one carrying the
191/// class name (so handlers can match on it); any other expression is
192/// assumed to already be a PyException.
193fn exception_value(
194    exc: &ExprType,
195    ctx: CodeGenContext,
196    options: PythonOptions,
197    symbols: SymbolTableScopes,
198) -> Result<TokenStream, Box<dyn std::error::Error>> {
199    match exc {
200        ExprType::Call(call) => {
201            if let ExprType::Name(name) = call.func.as_ref() {
202                if is_exception_class_name(&name.id) {
203                    let kind = &name.id;
204                    let msg = match call.args.len() {
205                        0 => quote!(String::new()),
206                        1 => {
207                            let arg = call.args[0].clone().to_rust(ctx, options, symbols)?;
208                            quote!(format!("{}", #arg))
209                        }
210                        _ => {
211                            let args: Result<Vec<TokenStream>, Box<dyn std::error::Error>> = call
212                                .args
213                                .iter()
214                                .map(|a| {
215                                    a.clone().to_rust(
216                                        ctx.clone(),
217                                        options.clone(),
218                                        symbols.clone(),
219                                    )
220                                })
221                                .collect();
222                            let args = args?;
223                            let fmt = vec!["{}"; args.len()].join(", ");
224                            quote!(format!(#fmt, #(#args),*))
225                        }
226                    };
227                    return Ok(quote!(PyException::new(#kind, #msg)));
228                }
229            }
230            let tokens = exc.clone().to_rust(ctx, options, symbols)?;
231            Ok(quote!(#tokens))
232        }
233        ExprType::Name(name) if is_exception_class_name(&name.id) => {
234            let kind = &name.id;
235            Ok(quote!(PyException::new(#kind, String::new())))
236        }
237        other => {
238            let tokens = other.clone().to_rust(ctx, options, symbols)?;
239            Ok(quote!(#tokens))
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    // Tests would go here - currently commented out as they need full AST infrastructure
247    // create_parse_test!(test_simple_raise, "raise ValueError('error')", "test.py");
248}