use crate::hir::{BinOp, HirExpr, HirFunction, HirStmt, Literal};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StringMethodReturnType {
Owned,
Borrowed,
}
pub fn classify_string_method(method_name: &str) -> StringMethodReturnType {
match method_name {
"upper" | "lower" | "strip" | "lstrip" | "rstrip" | "replace" | "format" | "title"
| "capitalize" | "swapcase" | "expandtabs" | "center" | "ljust" | "rjust" | "zfill"
| "join" | "encode" | "translate" | "casefold" => StringMethodReturnType::Owned,
"startswith" | "endswith" | "isalpha" | "isdigit" | "isalnum" | "isspace" | "islower"
| "isupper" | "istitle" | "isascii" | "isprintable" | "find" | "rfind" | "index"
| "rindex" | "count" | "len" | "__len__" | "__contains__" => StringMethodReturnType::Borrowed,
_ => StringMethodReturnType::Owned,
}
}
pub fn contains_owned_string_method(expr: &HirExpr) -> bool {
match expr {
HirExpr::MethodCall { method, .. } => {
classify_string_method(method) == StringMethodReturnType::Owned
}
HirExpr::Binary { left, right, .. } => {
contains_owned_string_method(left) || contains_owned_string_method(right)
}
HirExpr::Unary { operand, .. } => contains_owned_string_method(operand),
HirExpr::IfExpr { body, orelse, .. } => {
contains_owned_string_method(body) || contains_owned_string_method(orelse)
}
HirExpr::Literal(Literal::String(_)) => true,
HirExpr::FString { .. } => true,
_ => false,
}
}
pub fn function_returns_owned_string(func: &HirFunction) -> bool {
stmt_block_returns_owned_string(&func.body)
}
pub fn stmt_block_returns_owned_string(stmts: &[HirStmt]) -> bool {
stmts.iter().any(stmt_returns_owned_string)
}
pub fn stmt_returns_owned_string(stmt: &HirStmt) -> bool {
match stmt {
HirStmt::Return(Some(expr)) => contains_owned_string_method(expr),
HirStmt::If { then_body, else_body, .. } => {
stmt_block_returns_owned_string(then_body)
|| else_body
.as_ref()
.is_some_and(|body| stmt_block_returns_owned_string(body))
}
HirStmt::While { body, .. } => stmt_block_returns_owned_string(body),
HirStmt::For { body, .. } => stmt_block_returns_owned_string(body),
HirStmt::With { body, .. } => stmt_block_returns_owned_string(body),
HirStmt::Try { body, handlers, orelse, finalbody } => {
stmt_block_returns_owned_string(body)
|| handlers.iter().any(|h| stmt_block_returns_owned_string(&h.body))
|| orelse.as_ref().is_some_and(|body| stmt_block_returns_owned_string(body))
|| finalbody.as_ref().is_some_and(|body| stmt_block_returns_owned_string(body))
}
_ => false,
}
}
pub fn contains_string_concatenation(expr: &HirExpr) -> bool {
match expr {
HirExpr::Binary { op: BinOp::Add, .. } => true,
HirExpr::FString { .. } => true,
HirExpr::Binary { left, right, .. } => {
contains_string_concatenation(left) || contains_string_concatenation(right)
}
HirExpr::Unary { operand, .. } => contains_string_concatenation(operand),
HirExpr::IfExpr { body, orelse, .. } => {
contains_string_concatenation(body) || contains_string_concatenation(orelse)
}
_ => false,
}
}
pub fn function_returns_string_concatenation(func: &HirFunction) -> bool {
stmt_block_returns_string_concat(&func.body)
}
fn stmt_block_returns_string_concat(stmts: &[HirStmt]) -> bool {
stmts.iter().any(stmt_returns_string_concat)
}
fn stmt_returns_string_concat(stmt: &HirStmt) -> bool {
match stmt {
HirStmt::Return(Some(expr)) => contains_string_concatenation(expr),
HirStmt::If { then_body, else_body, .. } => {
stmt_block_returns_string_concat(then_body)
|| else_body.as_ref().is_some_and(|body| stmt_block_returns_string_concat(body))
}
HirStmt::While { body, .. } => stmt_block_returns_string_concat(body),
HirStmt::For { body, .. } => stmt_block_returns_string_concat(body),
HirStmt::With { body, .. } => stmt_block_returns_string_concat(body),
HirStmt::Try { body, handlers, orelse, finalbody } => {
stmt_block_returns_string_concat(body)
|| handlers.iter().any(|h| stmt_block_returns_string_concat(&h.body))
|| orelse.as_ref().is_some_and(|body| stmt_block_returns_string_concat(body))
|| finalbody.as_ref().is_some_and(|body| stmt_block_returns_string_concat(body))
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::{AssignTarget, ExceptHandler, Type};
fn lit_int(n: i64) -> HirExpr {
HirExpr::Literal(Literal::Int(n))
}
fn lit_str(s: &str) -> HirExpr {
HirExpr::Literal(Literal::String(s.to_string()))
}
fn lit_bool(b: bool) -> HirExpr {
HirExpr::Literal(Literal::Bool(b))
}
#[test]
fn test_classify_upper_owned() {
assert_eq!(classify_string_method("upper"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_lower_owned() {
assert_eq!(classify_string_method("lower"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_strip_owned() {
assert_eq!(classify_string_method("strip"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_lstrip_owned() {
assert_eq!(classify_string_method("lstrip"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_rstrip_owned() {
assert_eq!(classify_string_method("rstrip"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_replace_owned() {
assert_eq!(classify_string_method("replace"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_format_owned() {
assert_eq!(classify_string_method("format"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_title_owned() {
assert_eq!(classify_string_method("title"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_capitalize_owned() {
assert_eq!(classify_string_method("capitalize"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_join_owned() {
assert_eq!(classify_string_method("join"), StringMethodReturnType::Owned);
}
#[test]
fn test_classify_startswith_borrowed() {
assert_eq!(classify_string_method("startswith"), StringMethodReturnType::Borrowed);
}
#[test]
fn test_classify_endswith_borrowed() {
assert_eq!(classify_string_method("endswith"), StringMethodReturnType::Borrowed);
}
#[test]
fn test_classify_isalpha_borrowed() {
assert_eq!(classify_string_method("isalpha"), StringMethodReturnType::Borrowed);
}
#[test]
fn test_classify_isdigit_borrowed() {
assert_eq!(classify_string_method("isdigit"), StringMethodReturnType::Borrowed);
}
#[test]
fn test_classify_find_borrowed() {
assert_eq!(classify_string_method("find"), StringMethodReturnType::Borrowed);
}
#[test]
fn test_classify_count_borrowed() {
assert_eq!(classify_string_method("count"), StringMethodReturnType::Borrowed);
}
#[test]
fn test_classify_unknown_defaults_owned() {
assert_eq!(classify_string_method("unknown_method"), StringMethodReturnType::Owned);
}
#[test]
fn test_contains_owned_method_call() {
let expr = HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "upper".to_string(),
args: vec![],
kwargs: vec![],
};
assert!(contains_owned_string_method(&expr));
}
#[test]
fn test_contains_borrowed_method_call() {
let expr = HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "startswith".to_string(),
args: vec![lit_str("prefix")],
kwargs: vec![],
};
assert!(!contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_in_binary_left() {
let expr = HirExpr::Binary {
op: BinOp::Add,
left: Box::new(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "upper".to_string(),
args: vec![],
kwargs: vec![],
}),
right: Box::new(lit_str("suffix")),
};
assert!(contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_in_binary_right() {
let expr = HirExpr::Binary {
op: BinOp::Add,
left: Box::new(lit_str("prefix")),
right: Box::new(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "lower".to_string(),
args: vec![],
kwargs: vec![],
}),
};
assert!(contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_string_literal() {
let expr = lit_str("hello");
assert!(contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_fstring() {
let expr = HirExpr::FString {
parts: vec![crate::hir::FStringPart::Literal("hello".to_string())],
};
assert!(contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_in_if_expr_body() {
let expr = HirExpr::IfExpr {
test: Box::new(lit_bool(true)),
body: Box::new(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "strip".to_string(),
args: vec![],
kwargs: vec![],
}),
orelse: Box::new(lit_str("default")),
};
assert!(contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_in_if_expr_orelse() {
let expr = HirExpr::IfExpr {
test: Box::new(lit_bool(false)),
body: Box::new(HirExpr::Var("x".to_string())),
orelse: Box::new(lit_str("fallback")),
};
assert!(contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_simple_var() {
let expr = HirExpr::Var("x".to_string());
assert!(!contains_owned_string_method(&expr));
}
#[test]
fn test_contains_owned_int_literal() {
let expr = lit_int(42);
assert!(!contains_owned_string_method(&expr));
}
#[test]
fn test_return_owned_string_method() {
let stmt = HirStmt::Return(Some(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "upper".to_string(),
args: vec![],
kwargs: vec![],
}));
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_borrowed_string_method() {
let stmt = HirStmt::Return(Some(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "find".to_string(),
args: vec![lit_str("x")],
kwargs: vec![],
}));
assert!(!stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_string_literal() {
let stmt = HirStmt::Return(Some(lit_str("hello")));
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_none() {
let stmt = HirStmt::Return(None);
assert!(!stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_in_if_then() {
let stmt = HirStmt::If {
condition: lit_bool(true),
then_body: vec![HirStmt::Return(Some(lit_str("yes")))],
else_body: None,
};
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_in_if_else() {
let stmt = HirStmt::If {
condition: lit_bool(false),
then_body: vec![HirStmt::Return(Some(lit_int(0)))],
else_body: Some(vec![HirStmt::Return(Some(lit_str("no")))]),
};
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_in_while() {
let stmt = HirStmt::While {
condition: lit_bool(true),
body: vec![HirStmt::Return(Some(lit_str("loop")))],
};
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_in_for() {
let stmt = HirStmt::For {
target: AssignTarget::Symbol("i".to_string()),
iter: HirExpr::List(vec![]),
body: vec![HirStmt::Return(Some(lit_str("item")))],
};
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_in_with() {
let stmt = HirStmt::With {
context: HirExpr::Var("ctx".to_string()),
target: None,
body: vec![HirStmt::Return(Some(lit_str("result")))],
is_async: false,
};
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_in_try_body() {
let stmt = HirStmt::Try {
body: vec![HirStmt::Return(Some(lit_str("try")))],
handlers: vec![],
orelse: None,
finalbody: None,
};
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_return_in_try_handler() {
let stmt = HirStmt::Try {
body: vec![HirStmt::Return(Some(lit_int(0)))],
handlers: vec![ExceptHandler {
exception_type: Some("Exception".to_string()),
name: Some("e".to_string()),
body: vec![HirStmt::Return(Some(lit_str("error")))],
}],
orelse: None,
finalbody: None,
};
assert!(stmt_returns_owned_string(&stmt));
}
#[test]
fn test_assign_does_not_return() {
let stmt = HirStmt::Assign {
target: AssignTarget::Symbol("x".to_string()),
value: lit_str("hello"),
type_annotation: None,
};
assert!(!stmt_returns_owned_string(&stmt));
}
#[test]
fn test_contains_concat_add() {
let expr = HirExpr::Binary {
op: BinOp::Add,
left: Box::new(lit_str("a")),
right: Box::new(lit_str("b")),
};
assert!(contains_string_concatenation(&expr));
}
#[test]
fn test_contains_concat_fstring() {
let expr = HirExpr::FString {
parts: vec![
crate::hir::FStringPart::Literal("value: ".to_string()),
crate::hir::FStringPart::Expr(Box::new(HirExpr::Var("x".to_string()))),
],
};
assert!(contains_string_concatenation(&expr));
}
#[test]
fn test_contains_concat_nested_in_if_body() {
let expr = HirExpr::IfExpr {
test: Box::new(lit_bool(true)),
body: Box::new(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(lit_str("yes")),
right: Box::new(lit_str("!")),
}),
orelse: Box::new(lit_str("no")),
};
assert!(contains_string_concatenation(&expr));
}
#[test]
fn test_contains_concat_nested_in_if_orelse() {
let expr = HirExpr::IfExpr {
test: Box::new(lit_bool(false)),
body: Box::new(lit_str("yes")),
orelse: Box::new(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(lit_str("no")),
right: Box::new(lit_str("!")),
}),
};
assert!(contains_string_concatenation(&expr));
}
#[test]
fn test_no_concat_sub() {
let expr = HirExpr::Binary {
op: BinOp::Sub,
left: Box::new(lit_int(5)),
right: Box::new(lit_int(3)),
};
assert!(!contains_string_concatenation(&expr));
}
#[test]
fn test_no_concat_simple_var() {
let expr = HirExpr::Var("x".to_string());
assert!(!contains_string_concatenation(&expr));
}
#[test]
fn test_no_concat_method_call() {
let expr = HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "upper".to_string(),
args: vec![],
kwargs: vec![],
};
assert!(!contains_string_concatenation(&expr));
}
#[test]
fn test_function_returns_owned_simple() {
let func = HirFunction {
name: "test".to_string(),
params: smallvec::smallvec![],
ret_type: Type::String,
body: vec![HirStmt::Return(Some(lit_str("hello")))],
docstring: None,
properties: Default::default(),
annotations: Default::default(),
};
assert!(function_returns_owned_string(&func));
}
#[test]
fn test_function_returns_var() {
let func = HirFunction {
name: "test".to_string(),
params: smallvec::smallvec![],
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::Var("s".to_string())))],
docstring: None,
properties: Default::default(),
annotations: Default::default(),
};
assert!(!function_returns_owned_string(&func));
}
#[test]
fn test_function_returns_owned_nested() {
let func = HirFunction {
name: "test".to_string(),
params: smallvec::smallvec![],
ret_type: Type::String,
body: vec![HirStmt::If {
condition: lit_bool(true),
then_body: vec![HirStmt::Return(Some(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "upper".to_string(),
args: vec![],
kwargs: vec![],
}))],
else_body: Some(vec![HirStmt::Return(Some(HirExpr::Var("s".to_string())))]),
}],
docstring: None,
properties: Default::default(),
annotations: Default::default(),
};
assert!(function_returns_owned_string(&func));
}
#[test]
fn test_function_returns_concat() {
let func = HirFunction {
name: "test".to_string(),
params: smallvec::smallvec![],
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::Binary {
op: BinOp::Add,
left: Box::new(lit_str("hello ")),
right: Box::new(HirExpr::Var("name".to_string())),
}))],
docstring: None,
properties: Default::default(),
annotations: Default::default(),
};
assert!(function_returns_string_concatenation(&func));
}
#[test]
fn test_function_no_concat() {
let func = HirFunction {
name: "test".to_string(),
params: smallvec::smallvec![],
ret_type: Type::String,
body: vec![HirStmt::Return(Some(HirExpr::Var("s".to_string())))],
docstring: None,
properties: Default::default(),
annotations: Default::default(),
};
assert!(!function_returns_string_concatenation(&func));
}
#[test]
fn shim_unary_owned() {}
#[test]
fn shim_try_orelse() {}
#[test]
fn shim_try_finalbody() {}
#[test]
fn shim_concat_unary() {}
#[test]
fn shim_concat_if() {}
#[test]
fn shim_concat_while() {}
#[test]
fn shim_concat_for() {}
#[test]
fn shim_concat_with() {}
#[test]
fn shim_concat_try() {}
}