Skip to main content

python_ast/parser/
mod.rs

1use crate::{dump, error_to_pyerr, parsing_error, Module, Name, Result as CrateResult, SourceLocation, *};
2
3use pyo3::prelude::*;
4use std::ffi::CString;
5
6use std::path::MAIN_SEPARATOR;
7
8/// Takes a string of Python code and emits a Python struct that represents the AST.
9fn parse_to_py(
10    input: impl AsRef<str>,
11    filename: impl AsRef<str>,
12    py: Python<'_>,
13) -> PyResult<Py<PyAny>> {
14    let pymodule_code = include_str!("__init__.py");
15
16    // We want to call tokenize.tokenize from Python.
17    let code_cstr = CString::new(pymodule_code)?;
18    let pymodule = PyModule::from_code(py, &code_cstr, c"__init__.py", c"parser")?;
19    let t = pymodule.getattr("parse")?;
20    assert!(t.is_callable());
21    let args = (input.as_ref(), filename.as_ref());
22
23    let py_tree = t.call1(args)?;
24    tracing::debug!("py_tree: {}", dump(&py_tree, Some(4))?);
25
26    Ok(py_tree.into())
27}
28
29/// Parses Python code and returns the AST as a Module with improved error handling.
30/// 
31/// This function accepts any type that can be converted to a string reference,
32/// making it flexible for different input types. It provides detailed error information
33/// including file location and helpful guidance when parsing fails.
34/// 
35/// # Arguments
36/// * `input` - The Python source code to parse
37/// * `filename` - The filename to associate with the parsed code
38/// 
39/// # Returns
40/// * `CrateResult<Module>` - The parsed AST module or a detailed error with location info
41/// 
42/// # Examples
43/// ```rust
44/// use python_ast::parse_enhanced;
45/// 
46/// let code = "x = 1 + 2";
47/// let module = parse_enhanced(code, "example.py").unwrap();
48/// ```
49pub fn parse_enhanced(input: impl AsRef<str>, filename: impl AsRef<str>) -> CrateResult<Module> {
50    let filename = filename.as_ref();
51    let input_str = input.as_ref();
52    let location = SourceLocation::new(filename);
53    
54    // Empty files are valid in Python (they create empty modules), so we don't treat them as errors
55    
56    let mut module: Module = Python::attach(|py| {
57        let py_tree = parse_to_py(input_str, filename, py)
58            .map_err(|py_err| {
59                // Convert PyO3 errors to our more detailed error format.
60                // The PyErr is preserved as a structured `py_err` debug field
61                // (instead of being prematurely stringified into a message)
62                // so tracing subscribers can see the original Python error
63                // object.
64                let help_msg = if format!("{}", py_err).contains("IndentationError") {
65                    "Fix indentation issues. Python requires consistent indentation (use either spaces or tabs, not both)."
66                } else if format!("{}", py_err).contains("SyntaxError") {
67                    "Check your Python syntax. Common issues include missing colons, incorrect indentation, or unclosed brackets."
68                } else {
69                    "Ensure the input contains valid Python code. Check for syntax errors or unsupported constructs."
70                };
71
72                // Put the Python error text in the message itself so consumers
73                // (the proc macro in particular) can show the real cause; the
74                // structured PyErr is still attached as a debug field.
75                parsing_error(
76                    location.clone(),
77                    format!("Python parsing failed: {}", py_err),
78                    help_msg,
79                )
80                .with_field_debug("py_err", &py_err)
81            })?;
82
83        py_tree.extract(py)
84            .map_err(|py_err: pyo3::PyErr| {
85                // The extraction layer produces precise "cannot convert X at
86                // line N" errors; surface that text as the message rather than
87                // burying it under a generic one.
88                let cause = py_err
89                    .value(py)
90                    .str()
91                    .map(|s| s.to_string())
92                    .unwrap_or_else(|_| py_err.to_string());
93                parsing_error(
94                    location.clone(),
95                    cause,
96                    "This Python construct is not yet supported by rython. Rewrite it using supported constructs, or file an issue."
97                )
98                .with_field_debug("py_err", &py_err)
99            })
100    })?;
101
102    module.filename = Some(filename.into());
103
104    if let Some(name_str) = filename.replace(MAIN_SEPARATOR, "__").strip_suffix(".py") {
105        module.name = Some(Name::try_from(name_str).map_err(|_| {
106            parsing_error(
107                location,
108                "Invalid module name derived from filename",
109                "Use a valid Python identifier for the filename (without special characters except underscores).",
110            )
111            .with_field("name", name_str)
112        })?);
113    }
114
115    tracing::debug!("module: {:#?}", module);
116    for item in module.__dir__() {
117        tracing::debug!("module.__dir__: {:#?}", item.as_ref());
118    }
119    Ok(module)
120}
121
122/// Parses Python code and returns the AST as a Module (backward compatible version).
123/// 
124/// This is the original parse function that returns PyResult for backward compatibility.
125/// For better error messages with location information, use `parse_enhanced` instead.
126/// 
127/// # Arguments
128/// * `input` - The Python source code to parse
129/// * `filename` - The filename to associate with the parsed code
130/// 
131/// # Returns
132/// * `PyResult<Module>` - The parsed AST module or a PyO3 error
133/// 
134/// # Examples
135/// ```rust
136/// use python_ast::parse;
137/// 
138/// let code = "x = 1 + 2";
139/// let module = parse(code, "example.py").unwrap();
140/// ```
141pub fn parse(input: impl AsRef<str>, filename: impl AsRef<str>) -> PyResult<Module> {
142    // Use the enhanced version but convert the error type for backward compatibility
143    parse_enhanced(input, filename).map_err(error_to_pyerr)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_parse_simple_expression() {
152        let code = "1 + 2";
153        let result = parse(code, "test.py");
154        assert!(result.is_ok());
155        
156        let module = result.unwrap();
157        assert!(module.filename.is_some());
158        assert_eq!(module.filename.as_ref().unwrap(), "test.py");
159        assert!(!module.raw.body.is_empty());
160    }
161
162    #[test]
163    fn test_parse_function_definition() {
164        let code = r#"
165def hello_world():
166    return "Hello, World!"
167"#;
168        let result = parse(code, "function_test.py");
169        assert!(result.is_ok());
170        
171        let module = result.unwrap();
172        assert_eq!(module.raw.body.len(), 1);
173    }
174
175    #[test]
176    fn test_parse_class_definition() {
177        let code = r#"
178class TestClass:
179    def __init__(self):
180        self.value = 42
181        
182    def get_value(self):
183        return self.value
184"#;
185        let result = parse(code, "class_test.py");
186        assert!(result.is_ok());
187        
188        let module = result.unwrap();
189        assert_eq!(module.raw.body.len(), 1);
190    }
191
192    #[test]
193    fn test_parse_import_statements() {
194        let code = r#"
195import os
196import sys
197from collections import defaultdict
198from typing import List, Dict
199"#;
200        let result = parse(code, "import_test.py");
201        assert!(result.is_ok());
202        
203        let module = result.unwrap();
204        assert_eq!(module.raw.body.len(), 4);
205    }
206
207    #[test]
208    fn test_parse_complex_expressions() {
209        let code = r#"
210result = [x**2 for x in range(10) if x % 2 == 0]
211data = {"key": value for key, value in items.items()}
212condition = (a > b) and (c < d) or (e == f)
213"#;
214        let result = parse(code, "expressions_test.py");
215        assert!(result.is_ok());
216        
217        let module = result.unwrap();
218        assert_eq!(module.raw.body.len(), 3);
219    }
220
221    #[test]
222    fn test_parse_control_flow() {
223        let code = r#"
224if condition:
225    for i in range(10):
226        if i % 2 == 0:
227            continue
228        else:
229            break
230else:
231    while True:
232        try:
233            do_something()
234        except Exception as e:
235            handle_error(e)
236        finally:
237            cleanup()
238"#;
239        let result = parse(code, "control_flow_test.py");
240        assert!(result.is_ok());
241        
242        let module = result.unwrap();
243        assert_eq!(module.raw.body.len(), 1);
244    }
245
246    #[test]
247    fn test_parse_async_code() {
248        let code = r#"
249async def async_function():
250    async with async_context():
251        result = await some_async_operation()
252        async for item in async_iterator:
253            yield item
254"#;
255        let result = parse(code, "async_test.py");
256        assert!(result.is_ok());
257        
258        let module = result.unwrap();
259        assert_eq!(module.raw.body.len(), 1);
260    }
261
262    #[test]
263    fn test_parse_decorators() {
264        let code = r#"
265@decorator
266@another_decorator(arg1, arg2)
267def decorated_function():
268    pass
269
270@property
271def getter(self):
272    return self._value
273"#;
274        let result = parse(code, "decorators_test.py");
275        assert!(result.is_ok());
276        
277        let module = result.unwrap();
278        assert_eq!(module.raw.body.len(), 2);
279    }
280
281    #[test]
282    fn test_parse_invalid_syntax() {
283        let code = "def invalid_function(";  // Missing closing parenthesis
284        let result = parse(code, "invalid.py");
285        assert!(result.is_err());
286    }
287
288    #[test]
289    fn test_parse_empty_file() {
290        let code = "";
291        let result = parse(code, "empty.py");
292        assert!(result.is_ok());
293        
294        let module = result.unwrap();
295        assert!(module.raw.body.is_empty());
296    }
297
298    #[test]
299    fn test_parse_comments_and_docstrings() {
300        let code = r#"
301"""Module docstring"""
302# This is a comment
303def function_with_docstring():
304    """Function docstring"""
305    pass  # Another comment
306"#;
307        let result = parse(code, "comments_test.py");
308        assert!(result.is_ok());
309        
310        let module = result.unwrap();
311        assert_eq!(module.raw.body.len(), 2); // Docstring + function
312    }
313
314    #[test]
315    fn test_module_name_generation() {
316        let result = parse("x = 1", "some_file.py");
317        assert!(result.is_ok());
318        
319        let module = result.unwrap();
320        assert!(module.name.is_some());
321        assert_eq!(module.name.unwrap().id, "some_file");
322    }
323
324    #[test]
325    fn test_module_name_with_path_separators() {
326        let code = "x = 1";
327        let filename = format!("path{}to{}module.py", std::path::MAIN_SEPARATOR, std::path::MAIN_SEPARATOR);
328        let result = parse(code, &filename);
329        assert!(result.is_ok());
330        
331        let module = result.unwrap();
332        assert!(module.name.is_some());
333        assert_eq!(module.name.unwrap().id, "path__to__module");
334    }
335}