use crate::Mutation;
use ryo_symbol::SymbolId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum EnumToTraitStrategy {
#[default]
Dynamic,
Static,
Generic,
MarkerOnly,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum MatchHandling {
#[default]
WarnOnly,
Downcast,
BlockOnMatch,
}
#[derive(Debug, Clone)]
pub struct ExtractTraitMutation {
pub symbol_id: SymbolId,
pub trait_name: String,
pub methods: Option<Vec<String>>,
}
impl ExtractTraitMutation {
pub fn new(symbol_id: SymbolId, trait_name: impl Into<String>) -> Self {
Self {
symbol_id,
trait_name: trait_name.into(),
methods: None,
}
}
pub fn with_methods(mut self, methods: Vec<String>) -> Self {
self.methods = Some(methods);
self
}
}
impl Mutation for ExtractTraitMutation {
fn describe(&self) -> String {
format!(
"Extract trait '{}' from impl (SymbolId: {:?})",
self.trait_name, self.symbol_id
)
}
fn mutation_type(&self) -> &'static str {
"ExtractTrait"
}
fn box_clone(&self) -> Box<dyn Mutation> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
pub struct InlineTraitMutation {
pub symbol_id: SymbolId,
pub struct_name: String,
pub remove_trait: bool, }
impl InlineTraitMutation {
pub fn new(symbol_id: SymbolId, struct_name: impl Into<String>) -> Self {
Self {
symbol_id,
struct_name: struct_name.into(),
remove_trait: true,
}
}
pub fn keep_trait(mut self) -> Self {
self.remove_trait = false;
self
}
}
impl Mutation for InlineTraitMutation {
fn describe(&self) -> String {
format!(
"Inline trait {:?} into impl {}",
self.symbol_id, self.struct_name
)
}
fn mutation_type(&self) -> &'static str {
"InlineTrait"
}
fn box_clone(&self) -> Box<dyn Mutation> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
pub struct RemoveTraitMutation {
pub trait_id: SymbolId,
}
impl RemoveTraitMutation {
pub fn new(trait_id: SymbolId) -> Self {
Self { trait_id }
}
}
impl Mutation for RemoveTraitMutation {
fn describe(&self) -> String {
format!("Remove trait {}", self.trait_id)
}
fn mutation_type(&self) -> &'static str {
"RemoveTrait"
}
fn box_clone(&self) -> Box<dyn Mutation> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
pub struct EnumToTraitMutation {
pub symbol_id: ryo_symbol::SymbolId,
pub trait_name: Option<String>,
pub remove_enum: bool,
pub variants: Vec<VariantInfo>,
pub strategy: EnumToTraitStrategy,
pub match_handling: MatchHandling,
}
#[derive(Debug, Clone)]
pub struct VariantInfo {
pub name: String,
pub fields: Vec<FieldInfo>,
}
#[derive(Debug, Clone)]
pub struct FieldInfo {
pub name: String,
pub ty: String,
}
impl EnumToTraitMutation {
pub fn from_symbol_id(symbol_id: ryo_symbol::SymbolId) -> Self {
Self {
symbol_id,
trait_name: None,
remove_enum: true,
variants: Vec::new(),
strategy: EnumToTraitStrategy::default(),
match_handling: MatchHandling::default(),
}
}
pub fn with_trait_name(mut self, name: impl Into<String>) -> Self {
self.trait_name = Some(name.into());
self
}
pub fn keep_enum(mut self) -> Self {
self.remove_enum = false;
self
}
pub fn with_variants(mut self, variants: Vec<VariantInfo>) -> Self {
self.variants = variants;
self
}
pub fn with_strategy(mut self, strategy: EnumToTraitStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_match_handling(mut self, handling: MatchHandling) -> Self {
self.match_handling = handling;
self
}
}
impl Mutation for EnumToTraitMutation {
fn describe(&self) -> String {
let trait_name = self.trait_name.as_deref().unwrap_or("<from_enum>");
format!(
"Convert enum (symbol:{:?}) to trait '{}' with {} struct implementations",
self.symbol_id,
trait_name,
self.variants.len()
)
}
fn mutation_type(&self) -> &'static str {
"EnumToTrait"
}
fn box_clone(&self) -> Box<dyn Mutation> {
Box::new(self.clone())
}
}
pub mod cross_crate_caller_pattern {
use ryo_source::pure::{
PureBlock, PureExpr, PureFn, PureGenericParam, PureGenerics, PureParam, PureStmt, PureType,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CallerPattern {
Dot,
Ufcs,
GenericBound,
DynDispatch,
Other,
}
pub fn classify_path_expr(path: &str, trait_name: &str) -> Option<CallerPattern> {
let needle_as = format!("as {}", trait_name);
let needle_prefix = format!("{}::", trait_name);
if path.contains(&needle_as) || path.starts_with(&needle_prefix) {
Some(CallerPattern::Ufcs)
} else {
None
}
}
pub fn classify_method_call(
expr: &PureExpr,
trait_methods: &[String],
) -> Option<CallerPattern> {
if let PureExpr::MethodCall { method, .. } = expr {
if trait_methods.iter().any(|m| m == method) {
return Some(CallerPattern::Dot);
}
}
None
}
pub fn classify_fn_generic_bound(generics: &PureGenerics, trait_name: &str) -> bool {
for param in &generics.params {
if let PureGenericParam::Type { bounds, .. } = param {
if bounds.iter().any(|b| b == trait_name) {
return true;
}
}
}
for w in &generics.where_clause {
if w.contains(trait_name) {
return true;
}
}
false
}
pub fn classify_fn_dyn_param(params: &[PureParam], trait_name: &str) -> bool {
params.iter().any(|p| match p {
PureParam::Typed { ty, .. } => type_contains_dyn_trait(ty, trait_name),
_ => false,
})
}
fn type_contains_dyn_trait(ty: &PureType, trait_name: &str) -> bool {
match ty {
PureType::TraitObject(bounds) => bounds.iter().any(|b| b == trait_name),
PureType::Ref { ty, .. } => type_contains_dyn_trait(ty, trait_name),
_ => false,
}
}
pub fn classify_fn_signature(f: &PureFn, trait_name: &str) -> Vec<CallerPattern> {
let mut out = Vec::new();
if classify_fn_generic_bound(&f.generics, trait_name) {
out.push(CallerPattern::GenericBound);
}
if classify_fn_dyn_param(&f.params, trait_name) {
out.push(CallerPattern::DynDispatch);
}
out
}
pub fn walk_expr<F: FnMut(&PureExpr)>(expr: &PureExpr, visitor: &mut F) {
visitor(expr);
match expr {
PureExpr::Lit(_)
| PureExpr::Path(_)
| PureExpr::Macro { .. }
| PureExpr::Other(_)
| PureExpr::Verbatim(_)
| PureExpr::Continue { .. } => {}
PureExpr::Binary { left, right, .. } => {
walk_expr(left, visitor);
walk_expr(right, visitor);
}
PureExpr::Unary { expr, .. }
| PureExpr::Field { expr, .. }
| PureExpr::Await(expr)
| PureExpr::Try(expr)
| PureExpr::Ref { expr, .. }
| PureExpr::Cast { expr, .. }
| PureExpr::Let { expr, .. } => walk_expr(expr, visitor),
PureExpr::Call { func, args } => {
walk_expr(func, visitor);
for a in args {
walk_expr(a, visitor);
}
}
PureExpr::MethodCall { receiver, args, .. } => {
walk_expr(receiver, visitor);
for a in args {
walk_expr(a, visitor);
}
}
PureExpr::Index { expr, index } => {
walk_expr(expr, visitor);
walk_expr(index, visitor);
}
PureExpr::Block { block, .. }
| PureExpr::Async { body: block, .. }
| PureExpr::Unsafe(block) => walk_block(block, visitor),
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
walk_expr(cond, visitor);
walk_block(then_branch, visitor);
if let Some(e) = else_branch {
walk_expr(e, visitor);
}
}
PureExpr::Match { expr, arms } => {
walk_expr(expr, visitor);
for arm in arms {
if let Some(g) = &arm.guard {
walk_expr(g, visitor);
}
walk_expr(&arm.body, visitor);
}
}
PureExpr::Loop { body, .. } => walk_block(body, visitor),
PureExpr::While { cond, body, .. } => {
walk_expr(cond, visitor);
walk_block(body, visitor);
}
PureExpr::For { expr, body, .. } => {
walk_expr(expr, visitor);
walk_block(body, visitor);
}
PureExpr::Return(opt) => {
if let Some(e) = opt {
walk_expr(e, visitor);
}
}
PureExpr::Break { expr, .. } => {
if let Some(e) = expr {
walk_expr(e, visitor);
}
}
PureExpr::Closure { body, .. } => walk_expr(body, visitor),
PureExpr::Struct { fields, rest, .. } => {
for (_, e) in fields {
walk_expr(e, visitor);
}
if let Some(r) = rest {
walk_expr(r, visitor);
}
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
walk_expr(e, visitor);
}
}
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
walk_expr(s, visitor);
}
if let Some(e) = end {
walk_expr(e, visitor);
}
}
PureExpr::Repeat { expr, len } => {
walk_expr(expr, visitor);
walk_expr(len, visitor);
}
}
}
pub fn walk_block<F: FnMut(&PureExpr)>(block: &PureBlock, visitor: &mut F) {
for stmt in &block.stmts {
match stmt {
PureStmt::Local {
init, else_branch, ..
} => {
if let Some(e) = init {
walk_expr(e, visitor);
}
if let Some(e) = else_branch {
walk_expr(e, visitor);
}
}
PureStmt::Semi(e) | PureStmt::Expr(e) => walk_expr(e, visitor),
PureStmt::Item(_) | PureStmt::Verbatim(_) => {}
}
}
}
pub fn scan_body_for_patterns(
body: &PureBlock,
trait_name: &str,
trait_methods: &[String],
) -> Vec<CallerPattern> {
let mut found_dot = false;
let mut found_ufcs = false;
walk_block(body, &mut |e| {
if let PureExpr::Path(p) = e {
if classify_path_expr(p, trait_name).is_some() {
found_ufcs = true;
}
}
if classify_method_call(e, trait_methods).is_some() {
found_dot = true;
}
});
let mut out = Vec::new();
if found_dot {
out.push(CallerPattern::Dot);
}
if found_ufcs {
out.push(CallerPattern::Ufcs);
}
out
}
pub fn classify_fn(
f: &PureFn,
trait_name: &str,
trait_methods: &[String],
) -> Vec<CallerPattern> {
let mut out = classify_fn_signature(f, trait_name);
out.extend(scan_body_for_patterns(&f.body, trait_name, trait_methods));
out
}
pub fn walk_expr_mut<F: FnMut(&mut PureExpr)>(expr: &mut PureExpr, visitor: &mut F) {
visitor(expr);
match expr {
PureExpr::Lit(_)
| PureExpr::Path(_)
| PureExpr::Macro { .. }
| PureExpr::Other(_)
| PureExpr::Verbatim(_)
| PureExpr::Continue { .. } => {}
PureExpr::Binary { left, right, .. } => {
walk_expr_mut(left, visitor);
walk_expr_mut(right, visitor);
}
PureExpr::Unary { expr, .. }
| PureExpr::Field { expr, .. }
| PureExpr::Await(expr)
| PureExpr::Try(expr)
| PureExpr::Ref { expr, .. }
| PureExpr::Cast { expr, .. }
| PureExpr::Let { expr, .. } => walk_expr_mut(expr, visitor),
PureExpr::Call { func, args } => {
walk_expr_mut(func, visitor);
for a in args {
walk_expr_mut(a, visitor);
}
}
PureExpr::MethodCall { receiver, args, .. } => {
walk_expr_mut(receiver, visitor);
for a in args {
walk_expr_mut(a, visitor);
}
}
PureExpr::Index { expr, index } => {
walk_expr_mut(expr, visitor);
walk_expr_mut(index, visitor);
}
PureExpr::Block { block, .. }
| PureExpr::Async { body: block, .. }
| PureExpr::Unsafe(block) => walk_block_mut(block, visitor),
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
walk_expr_mut(cond, visitor);
walk_block_mut(then_branch, visitor);
if let Some(e) = else_branch {
walk_expr_mut(e, visitor);
}
}
PureExpr::Match { expr, arms } => {
walk_expr_mut(expr, visitor);
for arm in arms {
if let Some(g) = &mut arm.guard {
walk_expr_mut(g, visitor);
}
walk_expr_mut(&mut arm.body, visitor);
}
}
PureExpr::Loop { body, .. } => walk_block_mut(body, visitor),
PureExpr::While { cond, body, .. } => {
walk_expr_mut(cond, visitor);
walk_block_mut(body, visitor);
}
PureExpr::For { expr, body, .. } => {
walk_expr_mut(expr, visitor);
walk_block_mut(body, visitor);
}
PureExpr::Return(opt) => {
if let Some(e) = opt {
walk_expr_mut(e, visitor);
}
}
PureExpr::Break { expr, .. } => {
if let Some(e) = expr {
walk_expr_mut(e, visitor);
}
}
PureExpr::Closure { body, .. } => walk_expr_mut(body, visitor),
PureExpr::Struct { fields, rest, .. } => {
for (_, e) in fields {
walk_expr_mut(e, visitor);
}
if let Some(r) = rest {
walk_expr_mut(r, visitor);
}
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
walk_expr_mut(e, visitor);
}
}
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
walk_expr_mut(s, visitor);
}
if let Some(e) = end {
walk_expr_mut(e, visitor);
}
}
PureExpr::Repeat { expr, len } => {
walk_expr_mut(expr, visitor);
walk_expr_mut(len, visitor);
}
}
}
pub fn walk_block_mut<F: FnMut(&mut PureExpr)>(block: &mut PureBlock, visitor: &mut F) {
for stmt in &mut block.stmts {
match stmt {
PureStmt::Local {
init, else_branch, ..
} => {
if let Some(e) = init {
walk_expr_mut(e, visitor);
}
if let Some(e) = else_branch {
walk_expr_mut(e, visitor);
}
}
PureStmt::Semi(e) | PureStmt::Expr(e) => walk_expr_mut(e, visitor),
PureStmt::Item(_) | PureStmt::Verbatim(_) => {}
}
}
}
pub fn rewrite_path_expr_ufcs(path: &mut String, trait_name: &str, struct_name: &str) -> bool {
let prefix = format!("{}::", trait_name);
if let Some(rest) = path.strip_prefix(&prefix) {
let new_path = format!("{}::{}", struct_name, rest);
*path = new_path;
return true;
}
let needle_as = format!("as {}>::", trait_name);
if let Some(idx_as) = path.find(&needle_as) {
let tail_start = idx_as + needle_as.len();
let tail = path[tail_start..].to_string();
*path = format!("{}::{}", struct_name, tail);
return true;
}
false
}
pub fn rewrite_body_ufcs(body: &mut PureBlock, trait_name: &str, struct_name: &str) -> usize {
let mut count = 0usize;
walk_block_mut(body, &mut |e| {
if let PureExpr::Path(p) = e {
if rewrite_path_expr_ufcs(p, trait_name, struct_name) {
count += 1;
}
}
});
count
}
pub fn substitute_trait_in_type(
ty: &mut PureType,
trait_name: &str,
struct_name: &str,
) -> bool {
match ty {
PureType::TraitObject(bounds) => {
if bounds.iter().any(|b| b == trait_name) {
*ty = PureType::Path(struct_name.to_string());
true
} else {
false
}
}
PureType::Ref {
ty: inner,
lifetime: _,
is_mut: _,
} => substitute_trait_in_type(inner, trait_name, struct_name),
PureType::Tuple(items) => {
let mut changed = false;
for t in items {
if substitute_trait_in_type(t, trait_name, struct_name) {
changed = true;
}
}
changed
}
PureType::Slice(inner) => substitute_trait_in_type(inner, trait_name, struct_name),
PureType::Array { ty: inner, .. } => {
substitute_trait_in_type(inner, trait_name, struct_name)
}
PureType::Fn { params, ret } => {
let mut changed = false;
for t in params {
if substitute_trait_in_type(t, trait_name, struct_name) {
changed = true;
}
}
if let Some(r) = ret {
if substitute_trait_in_type(r, trait_name, struct_name) {
changed = true;
}
}
changed
}
PureType::Path(_)
| PureType::ImplTrait(_)
| PureType::Infer
| PureType::Never
| PureType::Other(_) => false,
}
}
pub fn rewrite_fn_dyn_dispatch(f: &mut PureFn, trait_name: &str, struct_name: &str) -> usize {
let mut count = 0usize;
for p in &mut f.params {
if let PureParam::Typed { ty, .. } = p {
if substitute_trait_in_type(ty, trait_name, struct_name) {
count += 1;
}
}
}
if let Some(ret) = &mut f.ret {
if substitute_trait_in_type(ret, trait_name, struct_name) {
count += 1;
}
}
count
}
pub fn substitute_generic_in_type(
ty: &mut PureType,
generic_name: &str,
struct_name: &str,
) -> bool {
match ty {
PureType::Path(p) => {
if p == generic_name {
*p = struct_name.to_string();
true
} else {
false
}
}
PureType::Ref { ty: inner, .. } => {
substitute_generic_in_type(inner, generic_name, struct_name)
}
PureType::Tuple(items) => {
let mut changed = false;
for t in items {
if substitute_generic_in_type(t, generic_name, struct_name) {
changed = true;
}
}
changed
}
PureType::Slice(inner) => substitute_generic_in_type(inner, generic_name, struct_name),
PureType::Array { ty: inner, .. } => {
substitute_generic_in_type(inner, generic_name, struct_name)
}
PureType::Fn { params, ret } => {
let mut changed = false;
for t in params {
if substitute_generic_in_type(t, generic_name, struct_name) {
changed = true;
}
}
if let Some(r) = ret {
if substitute_generic_in_type(r, generic_name, struct_name) {
changed = true;
}
}
changed
}
PureType::TraitObject(_)
| PureType::ImplTrait(_)
| PureType::Infer
| PureType::Never
| PureType::Other(_) => false,
}
}
pub fn rewrite_path_expr_generic(
path: &mut String,
generic_name: &str,
struct_name: &str,
) -> bool {
if path == generic_name {
*path = struct_name.to_string();
return true;
}
let prefix = format!("{}::", generic_name);
if let Some(rest) = path.strip_prefix(&prefix) {
let new_path = format!("{}::{}", struct_name, rest);
*path = new_path;
return true;
}
false
}
pub fn rewrite_body_generic_bound(
body: &mut PureBlock,
generic_name: &str,
struct_name: &str,
) -> usize {
let mut count = 0usize;
walk_block_mut(body, &mut |e| {
if let PureExpr::Path(p) = e {
if rewrite_path_expr_generic(p, generic_name, struct_name) {
count += 1;
}
}
});
for stmt in &mut body.stmts {
if let PureStmt::Local { ty: Some(ty), .. } = stmt {
if substitute_generic_in_type(ty, generic_name, struct_name) {
count += 1;
}
}
}
count
}
pub fn rewrite_turbofish_generic(
s: &mut String,
generic_name: &str,
struct_name: &str,
) -> usize {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut count = 0usize;
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
if c.is_alphabetic() || c == '_' {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let ident: String = chars[start..i].iter().collect();
if ident == generic_name {
out.push_str(struct_name);
count += 1;
} else {
out.push_str(&ident);
}
} else {
out.push(c);
i += 1;
}
}
*s = out;
count
}
pub fn rewrite_body_turbofish_generic(
body: &mut PureBlock,
generic_name: &str,
struct_name: &str,
) -> usize {
let mut count = 0usize;
walk_block_mut(body, &mut |e| {
if let PureExpr::MethodCall {
turbofish: Some(s), ..
} = e
{
count += rewrite_turbofish_generic(s, generic_name, struct_name);
}
});
count
}
pub fn rewrite_where_clause_generic(
clause: &mut Vec<String>,
generic_name: &str,
struct_name: &str,
) -> usize {
let mut count = 0usize;
clause.retain_mut(|predicate| {
let trimmed = predicate.trim_start();
let lhs_end = trimmed.find(':').unwrap_or(trimmed.len());
let lhs = trimmed[..lhs_end].trim();
if lhs == generic_name {
count += 1;
return false;
}
let chars: Vec<char> = predicate.chars().collect();
let mut out = String::with_capacity(predicate.len());
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
if c.is_alphabetic() || c == '_' {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let ident: String = chars[start..i].iter().collect();
if ident == generic_name {
out.push_str(struct_name);
count += 1;
} else {
out.push_str(&ident);
}
} else {
out.push(c);
i += 1;
}
}
*predicate = out;
true
});
count
}
pub fn rewrite_fn_generic_bound(f: &mut PureFn, trait_name: &str, struct_name: &str) -> usize {
let bound_names: Vec<String> = f
.generics
.params
.iter()
.filter_map(|p| match p {
PureGenericParam::Type { name, bounds }
if bounds.iter().any(|b| b == trait_name) =>
{
Some(name.clone())
}
_ => None,
})
.collect();
if bound_names.is_empty() {
return 0;
}
let before = f.generics.params.len();
f.generics.params.retain(|p| match p {
PureGenericParam::Type { name, .. } => !bound_names.contains(name),
_ => true,
});
let removed = before - f.generics.params.len();
let mut substitutions = 0usize;
for bound_name in &bound_names {
for p in &mut f.params {
if let PureParam::Typed { ty, .. } = p {
if substitute_generic_in_type(ty, bound_name, struct_name) {
substitutions += 1;
}
}
}
if let Some(ret) = &mut f.ret {
if substitute_generic_in_type(ret, bound_name, struct_name) {
substitutions += 1;
}
}
}
let mut body_edits = 0usize;
for bound_name in &bound_names {
body_edits += rewrite_body_generic_bound(&mut f.body, bound_name, struct_name);
body_edits += rewrite_body_turbofish_generic(&mut f.body, bound_name, struct_name);
body_edits +=
rewrite_where_clause_generic(&mut f.generics.where_clause, bound_name, struct_name);
}
removed + substitutions + body_edits
}
}
#[cfg(test)]
mod cross_crate_caller_pattern_tests {
use super::cross_crate_caller_pattern::*;
use ryo_source::pure::{
PureBlock, PureExpr, PureFn, PureGenericParam, PureGenerics, PureParam, PureType, PureVis,
};
fn mk_fn(name: &str, generics: PureGenerics, params: Vec<PureParam>) -> PureFn {
PureFn {
attrs: Vec::new(),
vis: PureVis::Public,
is_async: false,
is_async_inferred: false,
is_const: false,
is_unsafe: false,
abi: None,
name: name.to_string(),
generics,
params,
ret: None,
body: PureBlock { stmts: Vec::new() },
}
}
#[test]
fn ufcs_path_detected() {
let p = "<Case01 as Case01Trait>::foo";
assert_eq!(
classify_path_expr(p, "Case01Trait"),
Some(CallerPattern::Ufcs)
);
assert_eq!(classify_path_expr(p, "OtherTrait"), None);
}
#[test]
fn ufcs_path_negative_case() {
let p = "Case01::foo";
assert_eq!(classify_path_expr(p, "Case01Trait"), None);
}
#[test]
fn ufcs_path_trait_qualified_form_detected() {
let p = "Case01Trait::foo";
assert_eq!(
classify_path_expr(p, "Case01Trait"),
Some(CallerPattern::Ufcs)
);
}
#[test]
fn dot_method_call_detected() {
let receiver = PureExpr::Path("s".to_string());
let expr = PureExpr::MethodCall {
receiver: Box::new(receiver),
method: "foo".to_string(),
turbofish: None,
args: Vec::new(),
};
let methods = vec!["foo".to_string(), "bar".to_string()];
assert_eq!(
classify_method_call(&expr, &methods),
Some(CallerPattern::Dot)
);
}
#[test]
fn dot_method_call_negative_when_name_mismatch() {
let expr = PureExpr::MethodCall {
receiver: Box::new(PureExpr::Path("s".to_string())),
method: "qux".to_string(),
turbofish: None,
args: Vec::new(),
};
let methods = vec!["foo".to_string()];
assert_eq!(classify_method_call(&expr, &methods), None);
}
#[test]
fn dot_method_call_negative_when_not_method_call() {
let expr = PureExpr::Path("foo".to_string());
let methods = vec!["foo".to_string()];
assert_eq!(classify_method_call(&expr, &methods), None);
}
#[test]
fn generic_bound_detected_inline() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: Vec::new(),
};
assert!(classify_fn_generic_bound(&g, "Case01Trait"));
assert!(!classify_fn_generic_bound(&g, "OtherTrait"));
}
#[test]
fn generic_bound_detected_where_clause() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: Vec::new(),
}],
where_clause: vec!["T: Case01Trait".to_string()],
};
assert!(classify_fn_generic_bound(&g, "Case01Trait"));
}
#[test]
fn dyn_param_detected() {
let params = vec![PureParam::Typed {
name: "t".to_string(),
ty: PureType::Ref {
lifetime: None,
is_mut: false,
ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
},
is_mut: false,
pat: None,
}];
assert!(classify_fn_dyn_param(¶ms, "Case01Trait"));
assert!(!classify_fn_dyn_param(¶ms, "OtherTrait"));
}
#[test]
fn dyn_param_negative_when_no_dyn() {
let params = vec![PureParam::Typed {
name: "t".to_string(),
ty: PureType::Path("Case01".to_string()),
is_mut: false,
pat: None,
}];
assert!(!classify_fn_dyn_param(¶ms, "Case01Trait"));
}
#[test]
fn classify_fn_signature_combines_axes() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: Vec::new(),
};
let params = vec![PureParam::Typed {
name: "t".to_string(),
ty: PureType::Ref {
lifetime: None,
is_mut: false,
ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
},
is_mut: false,
pat: None,
}];
let f = mk_fn("use_both", g, params);
let out = classify_fn_signature(&f, "Case01Trait");
assert!(out.contains(&CallerPattern::GenericBound));
assert!(out.contains(&CallerPattern::DynDispatch));
assert_eq!(out.len(), 2);
}
#[test]
fn classify_fn_signature_empty_for_unrelated_trait() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: Vec::new(),
};
let f = mk_fn("use_generic", g, Vec::new());
let out = classify_fn_signature(&f, "OtherTrait");
assert!(out.is_empty());
}
use ryo_source::pure::PureStmt;
fn empty_generics() -> PureGenerics {
PureGenerics {
params: Vec::new(),
where_clause: Vec::new(),
}
}
fn mk_method_call(receiver_name: &str, method: &str) -> PureExpr {
PureExpr::MethodCall {
receiver: Box::new(PureExpr::Path(receiver_name.to_string())),
method: method.to_string(),
turbofish: None,
args: Vec::new(),
}
}
fn fn_with_body(name: &str, body: PureBlock) -> PureFn {
PureFn {
attrs: Vec::new(),
vis: PureVis::Public,
is_async: false,
is_async_inferred: false,
is_const: false,
is_unsafe: false,
abi: None,
name: name.to_string(),
generics: empty_generics(),
params: Vec::new(),
ret: None,
body,
}
}
#[test]
fn walk_expr_visits_method_call_arguments() {
let inner = mk_method_call("s", "bar");
let outer = PureExpr::Call {
func: Box::new(PureExpr::Path("foo".to_string())),
args: vec![inner],
};
let mut visited = 0usize;
walk_expr(&outer, &mut |e| {
if matches!(e, PureExpr::MethodCall { .. }) {
visited += 1;
}
});
assert_eq!(visited, 1);
}
#[test]
fn scan_body_detects_dot() {
let body = PureBlock {
stmts: vec![PureStmt::Expr(mk_method_call("s", "foo"))],
};
let methods = vec!["foo".to_string()];
let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
assert_eq!(out, vec![CallerPattern::Dot]);
}
#[test]
fn scan_body_detects_ufcs_path() {
let body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::Call {
func: Box::new(PureExpr::Path("<Case01 as Case01Trait>::foo".to_string())),
args: vec![PureExpr::Path("s".to_string())],
})],
};
let out = scan_body_for_patterns(&body, "Case01Trait", &[]);
assert_eq!(out, vec![CallerPattern::Ufcs]);
}
#[test]
fn scan_body_detects_both_dot_and_ufcs() {
let body = PureBlock {
stmts: vec![
PureStmt::Semi(mk_method_call("s", "foo")),
PureStmt::Expr(PureExpr::Path("<Case01 as Case01Trait>::bar".to_string())),
],
};
let methods = vec!["foo".to_string()];
let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
assert!(out.contains(&CallerPattern::Dot));
assert!(out.contains(&CallerPattern::Ufcs));
assert_eq!(out.len(), 2);
}
#[test]
fn scan_body_detects_dot_in_nested_block() {
let inner_if = PureExpr::If {
cond: Box::new(PureExpr::Lit("true".to_string())),
then_branch: PureBlock {
stmts: vec![PureStmt::Expr(mk_method_call("s", "foo"))],
},
else_branch: Some(Box::new(PureExpr::Lit("0".to_string()))),
};
let body = PureBlock {
stmts: vec![PureStmt::Expr(inner_if)],
};
let methods = vec!["foo".to_string()];
let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
assert_eq!(out, vec![CallerPattern::Dot]);
}
#[test]
fn scan_body_no_pattern_when_unrelated() {
let body = PureBlock {
stmts: vec![PureStmt::Expr(mk_method_call("s", "qux"))],
};
let methods = vec!["foo".to_string()];
let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
assert!(out.is_empty());
}
#[test]
fn rewrite_path_expr_ufcs_trait_qualified() {
let mut p = String::from("Case01Trait::foo");
let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
assert!(changed);
assert_eq!(p, "Case01::foo");
}
#[test]
fn rewrite_path_expr_ufcs_literal_envelope() {
let mut p = String::from("<Case01 as Case01Trait>::foo");
let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
assert!(changed);
assert_eq!(p, "Case01::foo");
}
#[test]
fn rewrite_path_expr_ufcs_no_match_returns_false() {
let mut p = String::from("Case01::foo");
let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
assert!(!changed);
assert_eq!(p, "Case01::foo");
}
#[test]
fn rewrite_path_expr_ufcs_unrelated_trait_no_change() {
let mut p = String::from("OtherTrait::foo");
let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
assert!(!changed);
assert_eq!(p, "OtherTrait::foo");
}
#[test]
fn rewrite_body_ufcs_single_path() {
let mut body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::Call {
func: Box::new(PureExpr::Path("Case01Trait::foo".to_string())),
args: vec![PureExpr::Path("s".to_string())],
})],
};
let count = rewrite_body_ufcs(&mut body, "Case01Trait", "Case01");
assert_eq!(count, 1);
if let PureStmt::Expr(PureExpr::Call { func, .. }) = &body.stmts[0] {
if let PureExpr::Path(p) = func.as_ref() {
assert_eq!(p, "Case01::foo");
} else {
panic!("expected Path");
}
} else {
panic!("expected Call");
}
}
#[test]
fn rewrite_body_ufcs_multiple_paths_in_nested_block() {
let inner_if = PureExpr::If {
cond: Box::new(PureExpr::Lit("true".to_string())),
then_branch: PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::Call {
func: Box::new(PureExpr::Path("Case01Trait::bar".to_string())),
args: Vec::new(),
})],
},
else_branch: Some(Box::new(PureExpr::Lit("0".to_string()))),
};
let mut body = PureBlock {
stmts: vec![
PureStmt::Semi(PureExpr::Call {
func: Box::new(PureExpr::Path("Case01Trait::foo".to_string())),
args: vec![PureExpr::Path("s".to_string())],
}),
PureStmt::Expr(inner_if),
],
};
let count = rewrite_body_ufcs(&mut body, "Case01Trait", "Case01");
assert_eq!(count, 2);
}
#[test]
fn rewrite_body_ufcs_leaves_unrelated_paths_alone() {
let mut body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::Call {
func: Box::new(PureExpr::Path("OtherTrait::foo".to_string())),
args: Vec::new(),
})],
};
let count = rewrite_body_ufcs(&mut body, "Case01Trait", "Case01");
assert_eq!(count, 0);
}
#[test]
fn substitute_trait_in_type_dyn_object() {
let mut ty = PureType::TraitObject(vec!["Case01Trait".to_string()]);
let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
assert!(changed);
assert_eq!(ty, PureType::Path("Case01".to_string()));
}
#[test]
fn substitute_trait_in_type_ref_dyn() {
let mut ty = PureType::Ref {
lifetime: None,
is_mut: false,
ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
};
let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
assert!(changed);
match ty {
PureType::Ref { ty: inner, .. } => {
assert_eq!(*inner, PureType::Path("Case01".to_string()));
}
_ => panic!("expected Ref"),
}
}
#[test]
fn substitute_trait_in_type_unrelated() {
let mut ty = PureType::Path("u32".to_string());
let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
assert!(!changed);
assert_eq!(ty, PureType::Path("u32".to_string()));
}
#[test]
fn substitute_trait_in_type_tuple_mixed() {
let mut ty = PureType::Tuple(vec![
PureType::Ref {
lifetime: None,
is_mut: false,
ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
},
PureType::Path("u32".to_string()),
]);
let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
assert!(changed);
}
#[test]
fn rewrite_fn_dyn_dispatch_signature_param_and_ret() {
let dyn_ref = |bounds: &str| PureType::Ref {
lifetime: None,
is_mut: false,
ty: Box::new(PureType::TraitObject(vec![bounds.to_string()])),
};
let mut f = mk_fn(
"use_both",
PureGenerics {
params: Vec::new(),
where_clause: Vec::new(),
},
vec![PureParam::Typed {
name: "t".to_string(),
ty: dyn_ref("Case01Trait"),
is_mut: false,
pat: None,
}],
);
f.ret = Some(dyn_ref("Case01Trait"));
let count = rewrite_fn_dyn_dispatch(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 2);
if let PureParam::Typed { ty, .. } = &f.params[0] {
match ty {
PureType::Ref { ty: inner, .. } => {
assert_eq!(**inner, PureType::Path("Case01".to_string()));
}
_ => panic!("expected Ref"),
}
}
match &f.ret {
Some(PureType::Ref { ty: inner, .. }) => {
assert_eq!(**inner, PureType::Path("Case01".to_string()));
}
_ => panic!("expected Ref ret"),
}
}
#[test]
fn rewrite_fn_dyn_dispatch_no_change_when_unrelated() {
let mut f = mk_fn(
"use_nothing",
PureGenerics {
params: Vec::new(),
where_clause: Vec::new(),
},
vec![PureParam::Typed {
name: "x".to_string(),
ty: PureType::Path("u32".to_string()),
is_mut: false,
pat: None,
}],
);
let count = rewrite_fn_dyn_dispatch(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 0);
}
#[test]
fn substitute_generic_in_type_direct_path() {
let mut ty = PureType::Path("T".to_string());
let changed = substitute_generic_in_type(&mut ty, "T", "Case01");
assert!(changed);
assert_eq!(ty, PureType::Path("Case01".to_string()));
}
#[test]
fn substitute_generic_in_type_ref_path() {
let mut ty = PureType::Ref {
lifetime: None,
is_mut: false,
ty: Box::new(PureType::Path("T".to_string())),
};
let changed = substitute_generic_in_type(&mut ty, "T", "Case01");
assert!(changed);
}
#[test]
fn substitute_generic_in_type_unrelated_name_unchanged() {
let mut ty = PureType::Path("u32".to_string());
let changed = substitute_generic_in_type(&mut ty, "T", "Case01");
assert!(!changed);
assert_eq!(ty, PureType::Path("u32".to_string()));
}
#[test]
fn rewrite_fn_generic_bound_strips_param_and_substitutes_type() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: Vec::new(),
};
let mut f = mk_fn(
"use_generic",
g,
vec![PureParam::Typed {
name: "t".to_string(),
ty: PureType::Path("T".to_string()),
is_mut: false,
pat: None,
}],
);
f.ret = Some(PureType::Path("u32".to_string()));
let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 2);
assert!(f.generics.params.is_empty(), "generic params must be empty");
if let PureParam::Typed { ty, .. } = &f.params[0] {
assert_eq!(*ty, PureType::Path("Case01".to_string()));
} else {
panic!("expected Typed param");
}
}
#[test]
fn rewrite_fn_generic_bound_leaves_unrelated_generics_alone() {
let g = PureGenerics {
params: vec![
PureGenericParam::Type {
name: "U".to_string(),
bounds: vec!["OtherTrait".to_string()],
},
PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
},
],
where_clause: Vec::new(),
};
let mut f = mk_fn(
"use_other",
g,
vec![
PureParam::Typed {
name: "t".to_string(),
ty: PureType::Path("T".to_string()),
is_mut: false,
pat: None,
},
PureParam::Typed {
name: "u".to_string(),
ty: PureType::Path("U".to_string()),
is_mut: false,
pat: None,
},
],
);
f.ret = Some(PureType::Path("T".to_string()));
let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 3);
assert_eq!(f.generics.params.len(), 1, "U generic must remain");
if let PureGenericParam::Type { name, .. } = &f.generics.params[0] {
assert_eq!(name, "U");
} else {
panic!("expected Type param");
}
if let PureParam::Typed { ty, .. } = &f.params[1] {
assert_eq!(*ty, PureType::Path("U".to_string()));
}
}
#[test]
fn rewrite_path_expr_generic_bare() {
let mut p = String::from("T");
let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
assert!(changed);
assert_eq!(p, "Case01");
}
#[test]
fn rewrite_path_expr_generic_prefixed() {
let mut p = String::from("T::default");
let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
assert!(changed);
assert_eq!(p, "Case01::default");
}
#[test]
fn rewrite_path_expr_generic_nested_segments() {
let mut p = String::from("T::Item::sub");
let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
assert!(changed);
assert_eq!(p, "Case01::Item::sub");
}
#[test]
fn rewrite_path_expr_generic_no_match_when_unrelated() {
let mut p = String::from("Type::method");
let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
assert!(!changed);
assert_eq!(p, "Type::method");
}
#[test]
fn rewrite_path_expr_generic_no_match_different_generic_name() {
let mut p = String::from("U::new");
let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
assert!(!changed);
assert_eq!(p, "U::new");
}
#[test]
fn rewrite_body_generic_bound_path_in_call() {
let mut body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::Call {
func: Box::new(PureExpr::Path("T::default".to_string())),
args: Vec::new(),
})],
};
let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
assert_eq!(count, 1);
if let PureStmt::Expr(PureExpr::Call { func, .. }) = &body.stmts[0] {
if let PureExpr::Path(p) = func.as_ref() {
assert_eq!(p, "Case01::default");
} else {
panic!("expected Path");
}
} else {
panic!("expected Call");
}
}
#[test]
fn rewrite_body_generic_bound_local_type_annotation() {
let mut body = PureBlock {
stmts: vec![PureStmt::Local {
pattern: ryo_source::pure::PurePattern::Ident {
name: "x".to_string(),
is_mut: false,
by_ref: false,
},
ty: Some(PureType::Path("T".to_string())),
init: None,
else_branch: None,
}],
};
let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
assert_eq!(count, 1);
if let PureStmt::Local { ty: Some(ty), .. } = &body.stmts[0] {
assert_eq!(*ty, PureType::Path("Case01".to_string()));
} else {
panic!("expected Local with ty");
}
}
#[test]
fn rewrite_body_generic_bound_combined_path_and_local_ty() {
let mut body = PureBlock {
stmts: vec![PureStmt::Local {
pattern: ryo_source::pure::PurePattern::Ident {
name: "x".to_string(),
is_mut: false,
by_ref: false,
},
ty: Some(PureType::Path("T".to_string())),
init: Some(PureExpr::Call {
func: Box::new(PureExpr::Path("T::default".to_string())),
args: Vec::new(),
}),
else_branch: None,
}],
};
let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
assert_eq!(count, 2);
}
#[test]
fn rewrite_body_generic_bound_no_change_when_unrelated() {
let mut body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::Call {
func: Box::new(PureExpr::Path("U::new".to_string())),
args: Vec::new(),
})],
};
let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
assert_eq!(count, 0);
}
#[test]
fn rewrite_fn_generic_bound_with_body_path() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: Vec::new(),
};
let body = PureBlock {
stmts: vec![PureStmt::Local {
pattern: ryo_source::pure::PurePattern::Ident {
name: "x".to_string(),
is_mut: false,
by_ref: false,
},
ty: None,
init: Some(PureExpr::Call {
func: Box::new(PureExpr::Path("T::default".to_string())),
args: Vec::new(),
}),
else_branch: None,
}],
};
let mut f = fn_with_body("use_generic", body);
f.generics = g;
f.params = vec![PureParam::Typed {
name: "t".to_string(),
ty: PureType::Path("T".to_string()),
is_mut: false,
pat: None,
}];
f.ret = Some(PureType::Path("u32".to_string()));
let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 3);
assert!(f.generics.params.is_empty());
if let PureStmt::Local {
init: Some(PureExpr::Call { func, .. }),
..
} = &f.body.stmts[0]
{
if let PureExpr::Path(p) = func.as_ref() {
assert_eq!(p, "Case01::default");
} else {
panic!("expected Path");
}
} else {
panic!("expected Local with init");
}
}
#[test]
fn rewrite_turbofish_generic_bare_token() {
let mut s = String::from("< T >");
let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
assert_eq!(count, 1);
assert_eq!(s, "< Case01 >");
}
#[test]
fn rewrite_turbofish_generic_multi_args_only_t_changes() {
let mut s = String::from("< T , U >");
let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
assert_eq!(count, 1);
assert_eq!(s, "< Case01 , U >");
}
#[test]
fn rewrite_turbofish_generic_nested() {
let mut s = String::from("< Vec < T > >");
let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
assert_eq!(count, 1);
assert_eq!(s, "< Vec < Case01 > >");
}
#[test]
fn rewrite_turbofish_generic_does_not_match_prefix_identifier() {
let mut s = String::from("< Type >");
let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
assert_eq!(count, 0);
assert_eq!(s, "< Type >");
}
#[test]
fn rewrite_turbofish_generic_does_not_match_extended_identifier() {
let mut s = String::from("< Tx >");
let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
assert_eq!(count, 0);
assert_eq!(s, "< Tx >");
}
#[test]
fn rewrite_turbofish_generic_repeated_occurrences() {
let mut s = String::from("< T , T , T >");
let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
assert_eq!(count, 3);
assert_eq!(s, "< Case01 , Case01 , Case01 >");
}
#[test]
fn rewrite_body_turbofish_generic_in_method_call() {
let mut body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::MethodCall {
receiver: Box::new(PureExpr::Path("vec".to_string())),
method: "iter".to_string(),
turbofish: Some("< T >".to_string()),
args: Vec::new(),
})],
};
let count = rewrite_body_turbofish_generic(&mut body, "T", "Case01");
assert_eq!(count, 1);
if let PureStmt::Expr(PureExpr::MethodCall {
turbofish: Some(s), ..
}) = &body.stmts[0]
{
assert_eq!(s, "< Case01 >");
} else {
panic!("expected MethodCall with turbofish");
}
}
#[test]
fn rewrite_body_turbofish_generic_skips_unrelated() {
let mut body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::MethodCall {
receiver: Box::new(PureExpr::Path("vec".to_string())),
method: "iter".to_string(),
turbofish: Some("< U >".to_string()),
args: Vec::new(),
})],
};
let count = rewrite_body_turbofish_generic(&mut body, "T", "Case01");
assert_eq!(count, 0);
}
#[test]
fn rewrite_fn_generic_bound_with_body_turbofish() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: Vec::new(),
};
let body = PureBlock {
stmts: vec![PureStmt::Expr(PureExpr::MethodCall {
receiver: Box::new(PureExpr::Path("vec".to_string())),
method: "iter".to_string(),
turbofish: Some("< T >".to_string()),
args: Vec::new(),
})],
};
let mut f = fn_with_body("use_generic", body);
f.generics = g;
f.params = vec![PureParam::Typed {
name: "t".to_string(),
ty: PureType::Path("T".to_string()),
is_mut: false,
pat: None,
}];
let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 3);
assert!(f.generics.params.is_empty());
if let PureStmt::Expr(PureExpr::MethodCall {
turbofish: Some(s), ..
}) = &f.body.stmts[0]
{
assert_eq!(s, "< Case01 >");
} else {
panic!("expected MethodCall with turbofish");
}
}
#[test]
fn rewrite_where_clause_drops_predicate_whose_lhs_is_stripped() {
let mut clause = vec!["T: Clone".to_string()];
let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
assert_eq!(count, 1);
assert!(clause.is_empty(), "predicate should be dropped");
}
#[test]
fn rewrite_where_clause_keeps_predicate_with_different_lhs() {
let mut clause = vec!["U: Foo".to_string()];
let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
assert_eq!(count, 0);
assert_eq!(clause, vec!["U: Foo".to_string()]);
}
#[test]
fn rewrite_where_clause_substitutes_in_predicate_rhs() {
let mut clause = vec!["U: Foo<T>".to_string()];
let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
assert_eq!(count, 1);
assert_eq!(clause, vec!["U: Foo<Case01>".to_string()]);
}
#[test]
fn rewrite_where_clause_substitutes_repeated_in_rhs() {
let mut clause = vec!["U: Foo<T> + Bar<T>".to_string()];
let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
assert_eq!(count, 2);
assert_eq!(clause, vec!["U: Foo<Case01> + Bar<Case01>".to_string()]);
}
#[test]
fn rewrite_where_clause_does_not_match_prefix_identifier_in_rhs() {
let mut clause = vec!["U: Foo<Type>".to_string()];
let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
assert_eq!(count, 0);
assert_eq!(clause, vec!["U: Foo<Type>".to_string()]);
}
#[test]
fn rewrite_where_clause_mixed_drop_and_substitute() {
let mut clause = vec![
"T: Clone".to_string(),
"U: Foo<T>".to_string(),
"V: Bar".to_string(),
];
let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
assert_eq!(count, 2);
assert_eq!(
clause,
vec!["U: Foo<Case01>".to_string(), "V: Bar".to_string()]
);
}
#[test]
fn rewrite_where_clause_leading_whitespace_lhs() {
let mut clause = vec![" T: Clone".to_string()];
let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
assert_eq!(count, 1);
assert!(clause.is_empty());
}
#[test]
fn rewrite_fn_generic_bound_with_where_clause() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: vec!["T: Clone".to_string()],
};
let mut f = mk_fn(
"use_generic",
g,
vec![PureParam::Typed {
name: "t".to_string(),
ty: PureType::Path("T".to_string()),
is_mut: false,
pat: None,
}],
);
let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 3);
assert!(f.generics.params.is_empty());
assert!(f.generics.where_clause.is_empty());
}
#[test]
fn rewrite_fn_generic_bound_no_change_when_no_matching_bound() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["OtherTrait".to_string()],
}],
where_clause: Vec::new(),
};
let mut f = mk_fn("use_unrelated", g, Vec::new());
let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
assert_eq!(count, 0);
assert_eq!(f.generics.params.len(), 1);
}
#[test]
fn classify_fn_combines_signature_and_body() {
let g = PureGenerics {
params: vec![PureGenericParam::Type {
name: "T".to_string(),
bounds: vec!["Case01Trait".to_string()],
}],
where_clause: Vec::new(),
};
let dyn_param = PureParam::Typed {
name: "t".to_string(),
ty: PureType::Ref {
lifetime: None,
is_mut: false,
ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
},
is_mut: false,
pat: None,
};
let body = PureBlock {
stmts: vec![
PureStmt::Semi(PureExpr::Path("<Case01 as Case01Trait>::foo".to_string())),
PureStmt::Expr(mk_method_call("t", "foo")),
],
};
let mut f = fn_with_body("use_all", body);
f.generics = g;
f.params = vec![dyn_param];
let methods = vec!["foo".to_string()];
let out = classify_fn(&f, "Case01Trait", &methods);
assert!(out.contains(&CallerPattern::GenericBound));
assert!(out.contains(&CallerPattern::DynDispatch));
assert!(out.contains(&CallerPattern::Dot));
assert!(out.contains(&CallerPattern::Ufcs));
assert_eq!(out.len(), 4);
}
}