use crate::error::ExpressionError;
use crate::profile::{ExprProfile, SyntaxFeature};
use ruff_python_ast as ast;
use ruff_python_parser;
use std::collections::HashMap;
#[derive(Debug, Clone)]
#[must_use]
pub struct ParsedExpression {
pub(crate) ast: ast::Expr,
pub(crate) expr: String,
#[allow(dead_code)] source: String,
pub(crate) keyword_renames: HashMap<String, String>,
pub(crate) accessed_symbols: HashSet<String>,
pub(crate) called_functions: HashSet<String>,
pub(crate) local_bindings: HashSet<String>,
}
impl ParsedExpression {
pub fn expression(&self) -> &str {
&self.expr
}
pub fn accessed_symbols(&self) -> &HashSet<String> {
&self.accessed_symbols
}
pub fn called_functions(&self) -> &HashSet<String> {
&self.called_functions
}
pub fn local_bindings(&self) -> &HashSet<String> {
&self.local_bindings
}
}
use std::collections::HashSet;
const PYTHON_KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue",
"def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
"in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",
"with", "yield",
];
fn make_replacement(keyword: &str, source: &str) -> String {
let len = keyword.len();
for prefix in b'a'..=b'z' {
let mut replacement = String::with_capacity(len);
replacement.push(prefix as char);
for (_i, c) in keyword.chars().enumerate().skip(1) {
replacement.push(if c.is_alphabetic() { c } else { 'x' });
}
if replacement.len() == len
&& !source.contains(&replacement)
&& !PYTHON_KEYWORDS.contains(&replacement.as_str())
{
return replacement;
}
}
"x".repeat(len)
}
impl ParsedExpression {
pub fn new(expr: &str) -> Result<ParsedExpression, ExpressionError> {
Self::with_profile(expr, &ExprProfile::latest())
}
pub fn with_profile(
expr: &str,
profile: &ExprProfile,
) -> Result<ParsedExpression, ExpressionError> {
let expr_str = expr.trim();
if expr_str.is_empty() {
return Err(ExpressionError::new("Empty expression"));
}
if expr_str.len() > MAX_PARSE_INPUT_LEN {
return Err(ExpressionError::new(format!(
"Expression source length ({} bytes) exceeds maximum allowed ({} bytes)",
expr_str.len(),
MAX_PARSE_INPUT_LEN
)));
}
if expr_str.len() <= FAST_PATH_INPUT_LEN {
return parse_inner(expr, expr_str, profile);
}
let expr_owned = expr.to_string();
let profile_owned = profile.clone();
let handle = std::thread::Builder::new()
.name("openjd-expr-parse".into())
.stack_size(PARSER_THREAD_STACK_SIZE)
.spawn(move || {
let expr_str = expr_owned.trim();
parse_inner(&expr_owned, expr_str, &profile_owned)
})
.map_err(|e| ExpressionError::new(format!("Failed to spawn parser thread: {e}")))?;
match handle.join() {
Ok(result) => result,
Err(_) => Err(ExpressionError::new(
"Parser thread panicked while parsing expression",
)),
}
}
pub fn evaluate(
&self,
values: &crate::symbol_table::SymbolTable,
) -> Result<crate::value::ExprValue, crate::error::ExpressionError> {
EvalBuilder::new(self).evaluate(&[values])
}
pub fn evaluate_with_metrics(
&self,
symtabs: &[&crate::symbol_table::SymbolTable],
) -> Result<crate::eval::EvalResult, crate::error::ExpressionError> {
EvalBuilder::new(self).evaluate_with_metrics(symtabs)
}
pub fn with_library<'a>(
&'a self,
library: &'a crate::function_library::FunctionLibrary,
) -> EvalBuilder<'a> {
EvalBuilder::new(self).with_library(library)
}
pub fn with_memory_limit(&self, limit: usize) -> EvalBuilder<'_> {
EvalBuilder::new(self).with_memory_limit(limit)
}
pub fn with_operation_limit(&self, limit: usize) -> EvalBuilder<'_> {
EvalBuilder::new(self).with_operation_limit(limit)
}
pub fn with_path_format(&self, format: crate::path_mapping::PathFormat) -> EvalBuilder<'_> {
EvalBuilder::new(self).with_path_format(format)
}
pub fn with_target_type<'a>(
&'a self,
target_type: &'a crate::types::ExprType,
) -> EvalBuilder<'a> {
EvalBuilder::new(self).with_target_type(target_type)
}
fn evaluator<'a>(
&'a self,
symtabs: &'a [&'a crate::symbol_table::SymbolTable],
) -> crate::eval::Evaluator<'a> {
crate::eval::Evaluator::new(symtabs)
.with_keyword_renames(&self.keyword_renames)
.with_expr_source(&self.expr)
}
pub fn as_name_lookup(&self) -> Option<&str> {
if self.called_functions.is_empty()
&& self.local_bindings.is_empty()
&& self.accessed_symbols.len() == 1
{
if build_symbol_name(&self.ast, &self.keyword_renames).is_some() {
return self.accessed_symbols.iter().next().map(|s| s.as_str());
}
}
None
}
}
#[must_use]
pub struct EvalBuilder<'a> {
parsed: &'a ParsedExpression,
library: Option<&'a crate::function_library::FunctionLibrary>,
memory_limit: Option<usize>,
operation_limit: Option<usize>,
path_format: Option<crate::path_mapping::PathFormat>,
target_type: Option<&'a crate::types::ExprType>,
}
impl<'a> EvalBuilder<'a> {
fn new(parsed: &'a ParsedExpression) -> Self {
Self {
parsed,
library: None,
memory_limit: None,
operation_limit: None,
path_format: None,
target_type: None,
}
}
pub fn with_library(mut self, library: &'a crate::function_library::FunctionLibrary) -> Self {
self.library = Some(library);
self
}
pub fn with_memory_limit(mut self, limit: usize) -> Self {
self.memory_limit = Some(limit);
self
}
pub fn with_operation_limit(mut self, limit: usize) -> Self {
self.operation_limit = Some(limit);
self
}
pub fn with_path_format(mut self, format: crate::path_mapping::PathFormat) -> Self {
self.path_format = Some(format);
self
}
pub fn with_target_type(mut self, target_type: &'a crate::types::ExprType) -> Self {
self.target_type = Some(target_type);
self
}
fn build_evaluator(
&self,
symtabs: &'a [&'a crate::symbol_table::SymbolTable],
) -> crate::eval::Evaluator<'a> {
let mut ev = self.parsed.evaluator(symtabs);
if let Some(lib) = self.library {
ev = ev.with_library(lib);
}
if let Some(limit) = self.memory_limit {
ev = ev.with_memory_limit(limit);
}
if let Some(limit) = self.operation_limit {
ev = ev.with_operation_limit(limit);
}
if let Some(fmt) = self.path_format {
ev = ev.with_path_format(fmt);
}
if let Some(tt) = self.target_type {
ev = ev.with_target_type(tt);
}
ev
}
pub fn evaluate(
self,
symtabs: &'a [&'a crate::symbol_table::SymbolTable],
) -> Result<crate::value::ExprValue, crate::error::ExpressionError> {
let mut ev = self.build_evaluator(symtabs);
ev.evaluate(&self.parsed.ast)
}
pub fn evaluate_with_metrics(
self,
symtabs: &'a [&'a crate::symbol_table::SymbolTable],
) -> Result<crate::eval::EvalResult, crate::error::ExpressionError> {
let mut ev = self.build_evaluator(symtabs);
let value = ev.evaluate(&self.parsed.ast)?;
Ok(crate::eval::EvalResult {
value,
peak_memory: ev.peak_memory(),
operation_count: ev.operation_count(),
})
}
}
fn build_symbol_name(expr: &ast::Expr, renames: &HashMap<String, String>) -> Option<String> {
match expr {
ast::Expr::Name(n) => {
let name = n.id.as_str();
Some(
renames
.get(name)
.cloned()
.unwrap_or_else(|| name.to_string()),
)
}
ast::Expr::Attribute(a) => {
let base = build_symbol_name(&a.value, renames)?;
let attr = a.attr.as_str();
let attr_resolved = renames
.get(attr)
.cloned()
.unwrap_or_else(|| attr.to_string());
Some(format!("{base}.{attr_resolved}"))
}
_ => None,
}
}
pub const MAX_EXPRESSION_DEPTH: usize = 64;
const FAST_PATH_INPUT_LEN: usize = 200;
pub const MAX_PARSE_INPUT_LEN: usize = 64 * 1024;
const PARSER_THREAD_STACK_SIZE: usize = 32 * 1024 * 1024;
fn parse_inner(
raw: &str,
expr_str: &str,
profile: &ExprProfile,
) -> Result<ParsedExpression, ExpressionError> {
let mut source = expr_str.to_string();
let mut keyword_renames: HashMap<String, String> = HashMap::new();
let is_multiline = source.contains('\n');
loop {
let to_parse = if is_multiline {
format!("({})", source)
} else {
source.clone()
};
match ruff_python_parser::parse_expression(&to_parse) {
Ok(parsed) => {
let expr_node = parsed.into_expr();
validate_structure(&expr_node, expr_str, profile)?;
let mut accessed_symbols = HashSet::new();
let mut called_functions = HashSet::new();
let mut local_bindings = HashSet::new();
collect_symbols(
&expr_node,
&keyword_renames,
&mut accessed_symbols,
&mut called_functions,
&mut local_bindings,
);
check_comprehension_shadowing(&expr_node, &HashSet::new(), expr_str)?;
return Ok(ParsedExpression {
ast: expr_node,
source: raw.to_string(),
expr: expr_str.to_string(),
keyword_renames,
accessed_symbols,
called_functions,
local_bindings,
});
}
Err(e) => {
let mut found = false;
for kw in PYTHON_KEYWORDS {
let pattern = format!(".{kw}");
if let Some(pos) = source.find(&pattern) {
let after_pos = pos + 1 + kw.len();
let after_char = source.chars().nth(after_pos);
if after_char.is_none_or(|c| !c.is_alphanumeric() && c != '_') {
let replacement = keyword_renames
.iter()
.find(|(_, v)| v.as_str() == *kw)
.map(|(k, _)| k.clone())
.unwrap_or_else(|| {
let r = make_replacement(kw, &source);
keyword_renames.insert(r.clone(), kw.to_string());
r
});
source = format!(
"{}.{}{}",
&source[..pos],
replacement,
&source[after_pos..]
);
found = true;
break;
}
}
}
if !found {
let msg = format!("Syntax error: {}", e.error);
let start = e.location.start().to_usize();
let end = e.location.end().to_usize();
let (col, end_col) = if start == end && !source.is_empty() {
(0, source.len())
} else {
(start.min(source.len()), end.min(source.len()))
};
let mut err = ExpressionError::new(msg);
if !source.is_empty() {
err = err.with_span(&source, col, end_col.max(col + 1));
}
return Err(err);
}
}
}
}
}
fn validate_structure(
node: &ast::Expr,
source: &str,
profile: &ExprProfile,
) -> Result<(), ExpressionError> {
validate_structure_inner(node, source, 0, profile)
}
fn validate_structure_inner(
node: &ast::Expr,
source: &str,
depth: usize,
profile: &ExprProfile,
) -> Result<(), ExpressionError> {
if depth > MAX_EXPRESSION_DEPTH {
return Err(
ExpressionError::expression_too_deep(depth, MAX_EXPRESSION_DEPTH)
.with_node(source, node),
);
}
fn err(msg: &str, source: &str, node: &ast::Expr) -> Result<(), ExpressionError> {
Err(ExpressionError::new(msg).with_node(source, node))
}
match node {
ast::Expr::ListComp(lc) => {
if lc.generators.len() > 1 && !profile.allows_syntax(SyntaxFeature::MultipleForClauses)
{
return Err(ExpressionError::new(
"Multiple 'for' clauses in list comprehensions are not supported",
)
.with_span(source, 0, source.len()));
}
for gen in &lc.generators {
if matches!(&gen.target, ast::Expr::Tuple(_))
&& !profile.allows_syntax(SyntaxFeature::TupleUnpackingInComprehension)
{
return Err(ExpressionError::new(
"Tuple unpacking in list comprehension is not supported",
)
.with_node(source, &gen.target));
}
if gen.ifs.len() > 1 && !profile.allows_syntax(SyntaxFeature::MultipleIfClauses) {
return Err(ExpressionError::new(
"Multiple 'if' clauses in a list comprehension are not supported; combine with 'and'",
).with_span(source, 0, source.len()));
}
if let ast::Expr::Name(n) = &gen.target {
let name = n.id.as_str();
if !name.starts_with(|c: char| c.is_ascii_lowercase() || c == '_') {
return Err(ExpressionError::new(format!(
"Loop variable '{}' must start with a lowercase letter or underscore",
name
))
.with_node(source, &gen.target));
}
}
}
validate_structure_inner(&lc.elt, source, depth + 1, profile)?;
for gen in &lc.generators {
validate_structure_inner(&gen.iter, source, depth + 1, profile)?;
for if_clause in &gen.ifs {
validate_structure_inner(if_clause, source, depth + 1, profile)?;
}
}
}
ast::Expr::BinOp(b) => {
match b.op {
ast::Operator::BitAnd if !profile.allows_syntax(SyntaxFeature::BitwiseAnd) => {
return err("Bitwise AND (&) is not supported", source, node)
}
ast::Operator::BitOr if !profile.allows_syntax(SyntaxFeature::BitwiseOr) => {
return err("Bitwise OR (|) is not supported", source, node)
}
ast::Operator::BitXor if !profile.allows_syntax(SyntaxFeature::BitwiseXor) => {
return err("Bitwise XOR (^) is not supported", source, node)
}
ast::Operator::LShift if !profile.allows_syntax(SyntaxFeature::LeftShift) => {
return err("Left shift (<<) is not supported", source, node)
}
ast::Operator::RShift if !profile.allows_syntax(SyntaxFeature::RightShift) => {
return err("Right shift (>>) is not supported", source, node)
}
ast::Operator::MatMult if !profile.allows_syntax(SyntaxFeature::MatMult) => {
return err("Matrix multiply (@) is not supported", source, node)
}
_ => {}
}
validate_structure_inner(&b.left, source, depth + 1, profile)?;
validate_structure_inner(&b.right, source, depth + 1, profile)?;
}
ast::Expr::UnaryOp(u) => {
if matches!(u.op, ast::UnaryOp::Invert)
&& !profile.allows_syntax(SyntaxFeature::BitwiseNot)
{
return err("Bitwise NOT (~) is not supported", source, node);
}
validate_structure_inner(&u.operand, source, depth + 1, profile)?;
}
ast::Expr::BoolOp(b) => {
for v in &b.values {
validate_structure_inner(v, source, depth + 1, profile)?;
}
}
ast::Expr::Compare(c) => {
for op in &c.ops {
match op {
ast::CmpOp::Is if !profile.allows_syntax(SyntaxFeature::IsOperator) => {
return err("'is' operator is not supported; use '=='", source, node)
}
ast::CmpOp::IsNot if !profile.allows_syntax(SyntaxFeature::IsNotOperator) => {
return err("'is not' operator is not supported; use '!='", source, node)
}
_ => {}
}
}
validate_structure_inner(&c.left, source, depth + 1, profile)?;
if c.comparators.len() > MAX_EXPRESSION_DEPTH {
return Err(ExpressionError::expression_too_deep(
c.comparators.len(),
MAX_EXPRESSION_DEPTH,
)
.with_node(source, node));
}
for comp in &c.comparators {
validate_structure_inner(comp, source, depth + 1, profile)?;
}
}
ast::Expr::If(i) => {
validate_structure_inner(&i.test, source, depth + 1, profile)?;
validate_structure_inner(&i.body, source, depth + 1, profile)?;
validate_structure_inner(&i.orelse, source, depth + 1, profile)?;
}
ast::Expr::Call(c) => {
validate_structure_inner(&c.func, source, depth + 1, profile)?;
for arg in &c.arguments.args {
validate_structure_inner(arg, source, depth + 1, profile)?;
}
if !c.arguments.keywords.is_empty()
&& !profile.allows_syntax(SyntaxFeature::KeywordArguments)
{
return err("Keyword arguments are not supported", source, node);
}
}
ast::Expr::List(l) => {
for elt in &l.elts {
validate_structure_inner(elt, source, depth + 1, profile)?;
}
}
ast::Expr::Subscript(s) => {
validate_structure_inner(&s.value, source, depth + 1, profile)?;
validate_structure_inner(&s.slice, source, depth + 1, profile)?;
}
ast::Expr::Attribute(a) => {
validate_structure_inner(&a.value, source, depth + 1, profile)?;
}
ast::Expr::StringLiteral(s) => {
for part in &s.value {
if matches!(
part.flags.prefix(),
ruff_python_ast::str_prefix::StringLiteralPrefix::Unicode
) && !profile.allows_syntax(SyntaxFeature::UnicodeStringPrefix)
{
return Err(ExpressionError::new(
"Unicode string prefix u'...' is not supported. Use '...' or \"...\" instead."
).with_node(source, &ast::Expr::StringLiteral(s.clone())));
}
}
}
ast::Expr::BytesLiteral(b) if !profile.allows_syntax(SyntaxFeature::BytesLiteral) => {
return Err(ExpressionError::new(
"Byte strings (b'...') are not supported. Use '...' or \"...\" instead.",
)
.with_node(source, &ast::Expr::BytesLiteral(b.clone())));
}
ast::Expr::Named(_) if !profile.allows_syntax(SyntaxFeature::Walrus) => {
return err("Walrus operator (:=) is not supported", source, node)
}
ast::Expr::Lambda(_) if !profile.allows_syntax(SyntaxFeature::Lambda) => {
return err("Lambda expressions are not supported", source, node)
}
ast::Expr::Tuple(_) if !profile.allows_syntax(SyntaxFeature::TupleLiteral) => {
return err(
"Tuple literals are not supported; use a list instead",
source,
node,
)
}
ast::Expr::Dict(_) if !profile.allows_syntax(SyntaxFeature::DictLiteral) => {
return err("Dict literals are not supported", source, node)
}
ast::Expr::Set(_) if !profile.allows_syntax(SyntaxFeature::SetLiteral) => {
return err("Set literals are not supported", source, node)
}
ast::Expr::DictComp(_) if !profile.allows_syntax(SyntaxFeature::DictComprehension) => {
return err(
"Dict comprehensions are not supported; only list comprehensions are allowed",
source,
node,
)
}
ast::Expr::SetComp(_) if !profile.allows_syntax(SyntaxFeature::SetComprehension) => {
return err(
"Set comprehensions are not supported; only list comprehensions are allowed",
source,
node,
)
}
ast::Expr::Generator(_) if !profile.allows_syntax(SyntaxFeature::GeneratorExpression) => {
return err(
"Generator expressions are not supported; use a list comprehension",
source,
node,
)
}
ast::Expr::FString(_) if !profile.allows_syntax(SyntaxFeature::FString) => {
return err(
"f-strings are not supported; use string concatenation",
source,
node,
)
}
ast::Expr::EllipsisLiteral(_) if !profile.allows_syntax(SyntaxFeature::Ellipsis) => {
return err("Ellipsis (...) is not supported", source, node)
}
ast::Expr::Starred(_) if !profile.allows_syntax(SyntaxFeature::Starred) => {
return err("Star expressions are not supported", source, node)
}
ast::Expr::Await(_) if !profile.allows_syntax(SyntaxFeature::Await) => {
return err("Await expressions are not supported", source, node)
}
_ => {}
}
Ok(())
}
fn check_comprehension_shadowing(
node: &ast::Expr,
outer_scope: &HashSet<String>,
source: &str,
) -> Result<(), ExpressionError> {
match node {
ast::Expr::ListComp(lc) => {
let mut comp_bindings = HashSet::new();
for gen in &lc.generators {
if let ast::Expr::Name(n) = &gen.target {
let name = n.id.to_string();
if outer_scope.contains(&name) {
return Err(ExpressionError::new(
format!("List comprehension variable '{}' shadows an outer comprehension variable", name),
).with_span(source, 0, source.len()));
}
comp_bindings.insert(name);
}
}
let new_scope: HashSet<String> = outer_scope.union(&comp_bindings).cloned().collect();
check_comprehension_shadowing(&lc.elt, &new_scope, source)?;
for gen in &lc.generators {
check_comprehension_shadowing(&gen.iter, outer_scope, source)?;
for if_clause in &gen.ifs {
check_comprehension_shadowing(if_clause, &new_scope, source)?;
}
}
}
ast::Expr::BinOp(b) => {
check_comprehension_shadowing(&b.left, outer_scope, source)?;
check_comprehension_shadowing(&b.right, outer_scope, source)?;
}
ast::Expr::UnaryOp(u) => {
check_comprehension_shadowing(&u.operand, outer_scope, source)?;
}
ast::Expr::BoolOp(b) => {
for v in &b.values {
check_comprehension_shadowing(v, outer_scope, source)?;
}
}
ast::Expr::Compare(c) => {
check_comprehension_shadowing(&c.left, outer_scope, source)?;
for comp in &c.comparators {
check_comprehension_shadowing(comp, outer_scope, source)?;
}
}
ast::Expr::If(i) => {
check_comprehension_shadowing(&i.test, outer_scope, source)?;
check_comprehension_shadowing(&i.body, outer_scope, source)?;
check_comprehension_shadowing(&i.orelse, outer_scope, source)?;
}
ast::Expr::Call(c) => {
check_comprehension_shadowing(&c.func, outer_scope, source)?;
for arg in &c.arguments.args {
check_comprehension_shadowing(arg, outer_scope, source)?;
}
}
ast::Expr::List(l) => {
for elt in &l.elts {
check_comprehension_shadowing(elt, outer_scope, source)?;
}
}
ast::Expr::Subscript(s) => {
check_comprehension_shadowing(&s.value, outer_scope, source)?;
check_comprehension_shadowing(&s.slice, outer_scope, source)?;
}
_ => {}
}
Ok(())
}
fn collect_symbols(
node: &ast::Expr,
renames: &HashMap<String, String>,
symbols: &mut HashSet<String>,
functions: &mut HashSet<String>,
locals: &mut HashSet<String>,
) {
match node {
ast::Expr::Name(n) => {
let name = renames
.get(n.id.as_str())
.cloned()
.unwrap_or_else(|| n.id.to_string());
if name == "True"
|| name == "False"
|| name == "None"
|| name == "true"
|| name == "false"
|| name == "null"
{
return;
}
if !locals.contains(&name) {
symbols.insert(name);
}
}
ast::Expr::Attribute(a) => {
if let Some(full_name) = build_symbol_name(node, renames) {
let base = full_name.split('.').next().unwrap_or("");
if !locals.contains(base)
&& base != "True"
&& base != "False"
&& base != "None"
&& base != "true"
&& base != "false"
&& base != "null"
{
symbols.insert(full_name);
}
} else {
collect_symbols(&a.value, renames, symbols, functions, locals);
}
}
ast::Expr::Call(c) => {
match &*c.func {
ast::Expr::Name(n) => {
let name = renames
.get(n.id.as_str())
.cloned()
.unwrap_or_else(|| n.id.to_string());
functions.insert(name);
}
ast::Expr::Attribute(a) => {
let method = a.attr.as_str();
let method_resolved = renames
.get(method)
.cloned()
.unwrap_or_else(|| method.to_string());
functions.insert(method_resolved);
collect_symbols(&a.value, renames, symbols, functions, locals);
}
_ => collect_symbols(&c.func, renames, symbols, functions, locals),
}
for arg in &c.arguments.args {
collect_symbols(arg, renames, symbols, functions, locals);
}
}
ast::Expr::BinOp(b) => {
collect_symbols(&b.left, renames, symbols, functions, locals);
collect_symbols(&b.right, renames, symbols, functions, locals);
}
ast::Expr::UnaryOp(u) => {
collect_symbols(&u.operand, renames, symbols, functions, locals);
}
ast::Expr::BoolOp(b) => {
for v in &b.values {
collect_symbols(v, renames, symbols, functions, locals);
}
}
ast::Expr::Compare(c) => {
collect_symbols(&c.left, renames, symbols, functions, locals);
for comp in &c.comparators {
collect_symbols(comp, renames, symbols, functions, locals);
}
}
ast::Expr::If(i) => {
collect_symbols(&i.test, renames, symbols, functions, locals);
collect_symbols(&i.body, renames, symbols, functions, locals);
collect_symbols(&i.orelse, renames, symbols, functions, locals);
}
ast::Expr::List(l) => {
for elt in &l.elts {
collect_symbols(elt, renames, symbols, functions, locals);
}
}
ast::Expr::Subscript(s) => {
collect_symbols(&s.value, renames, symbols, functions, locals);
collect_symbols(&s.slice, renames, symbols, functions, locals);
}
ast::Expr::Slice(s) => {
if let Some(l) = &s.lower {
collect_symbols(l, renames, symbols, functions, locals);
}
if let Some(u) = &s.upper {
collect_symbols(u, renames, symbols, functions, locals);
}
if let Some(st) = &s.step {
collect_symbols(st, renames, symbols, functions, locals);
}
}
ast::Expr::ListComp(lc) => {
for gen in &lc.generators {
collect_symbols(&gen.iter, renames, symbols, functions, locals);
}
for gen in &lc.generators {
if let ast::Expr::Name(n) = &gen.target {
locals.insert(n.id.to_string());
}
}
collect_symbols(&lc.elt, renames, symbols, functions, locals);
for gen in &lc.generators {
for if_clause in &gen.ifs {
collect_symbols(if_clause, renames, symbols, functions, locals);
}
}
}
ast::Expr::Starred(s) => {
collect_symbols(&s.value, renames, symbols, functions, locals);
}
ast::Expr::NumberLiteral(_)
| ast::Expr::StringLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_)
| ast::Expr::BytesLiteral(_) => {}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_arithmetic() {
let parsed = ParsedExpression::new("1 + 2").unwrap();
assert!(matches!(parsed.ast, ast::Expr::BinOp(_)));
}
#[test]
fn parse_attribute_access() {
let parsed = ParsedExpression::new("Param.Name").unwrap();
assert!(matches!(parsed.ast, ast::Expr::Attribute(_)));
}
#[test]
fn parse_function_call() {
let parsed = ParsedExpression::new("len(Param.Files)").unwrap();
assert!(matches!(parsed.ast, ast::Expr::Call(_)));
}
#[test]
fn parse_contextual_keyword_if() {
let parsed = ParsedExpression::new("Param.if").unwrap();
assert!(matches!(parsed.ast, ast::Expr::Attribute(_)));
assert!(!parsed.keyword_renames.is_empty());
}
#[test]
fn parse_contextual_keyword_def() {
let parsed = ParsedExpression::new("Param.def").unwrap();
assert!(!parsed.keyword_renames.is_empty());
}
#[test]
fn parse_conditional() {
let parsed = ParsedExpression::new("Param.X if Param.Flag else Param.Y").unwrap();
assert!(matches!(parsed.ast, ast::Expr::If(_)));
}
#[test]
fn parse_empty_error() {
assert!(ParsedExpression::new("").is_err());
}
#[test]
fn parse_syntax_error() {
assert!(ParsedExpression::new("1 +").is_err());
}
#[test]
fn keyword_replacement_same_length() {
let parsed = ParsedExpression::new("Param.if").unwrap();
for (replacement, original) in &parsed.keyword_renames {
assert_eq!(
replacement.len(),
original.len(),
"Replacement '{}' must be same length as original '{}'",
replacement,
original
);
}
}
}