use ryo_analysis::context::AnalysisContext;
use ryo_analysis::{FlowKind, SymbolId, VarId, VarKind};
use super::{PerformanceDetails, PerformanceSuggest};
use crate::{
LintSeverity, MutationSpec, OpportunityId, SafetyLevel, Suggest, SuggestCategory,
SuggestLocation, SuggestOpportunity, SuggestResult,
};
pub struct UnnecessaryClone {
min_confidence: f32,
}
impl UnnecessaryClone {
pub fn new() -> Self {
Self {
min_confidence: 0.5,
}
}
pub fn with_min_confidence(mut self, threshold: f32) -> Self {
self.min_confidence = threshold.clamp(0.0, 1.0);
self
}
fn analyze_clone_necessity(
&self,
ctx: &AnalysisContext,
cloned_var: VarId,
) -> Option<CloneAnalysis> {
let dataflow = &ctx.dataflow_graph;
let outgoing_flows = dataflow.outgoing(cloned_var);
if outgoing_flows.is_empty() {
return Some(CloneAnalysis {
pattern: ClonePattern::ImmediatelyDropped,
confidence: 0.9,
suggestion: "Remove unused clone()".to_string(),
});
}
let mut has_move = false;
let mut has_mut_borrow = false;
let mut has_shared_borrow = false;
let mut has_return = false;
let mut has_argument = false;
for &flow_id in outgoing_flows {
if let Some(flow_data) = dataflow.flow(flow_id) {
match flow_data.kind {
FlowKind::Move => has_move = true,
FlowKind::MutBorrow => has_mut_borrow = true,
FlowKind::SharedBorrow => has_shared_borrow = true,
FlowKind::Return => has_return = true,
FlowKind::Argument => has_argument = true,
_ => {}
}
}
}
if has_shared_borrow && !has_move && !has_mut_borrow && !has_return {
let var_is_mut = dataflow.var(cloned_var).is_some_and(|v| v.is_mut);
if var_is_mut || self.source_is_consumed_or_mutated(ctx, cloned_var) {
return None;
}
return Some(CloneAnalysis {
pattern: ClonePattern::OnlySharedBorrow,
confidence: 0.7,
suggestion: "Consider using reference instead of clone()".to_string(),
});
}
if has_argument && !has_return && !has_move && !has_mut_borrow {
if let Some(analysis) = self.analyze_argument_escape(ctx, cloned_var) {
return Some(analysis);
}
return None;
}
None
}
fn analyze_argument_escape(
&self,
ctx: &AnalysisContext,
cloned_var: VarId,
) -> Option<CloneAnalysis> {
let dataflow = &ctx.dataflow_graph;
let var_data = dataflow.var(cloned_var)?;
let parent_fn = var_data.parent;
let callees: Vec<SymbolId> = ctx.code_graph.callees_of(parent_fn).collect();
if callees.is_empty() {
return None;
}
for callee_id in &callees {
let Some(fn_detail) = ctx.detail_store.function(*callee_id) else {
return None;
};
for param in &fn_detail.params {
if param.is_self {
continue;
}
let ty = param.ty.trim();
if !ty.starts_with('&') {
return None;
}
}
}
Some(CloneAnalysis {
pattern: ClonePattern::ArgumentNoEscape,
confidence: 0.7,
suggestion: "All called functions accept references - clone() is likely unnecessary"
.to_string(),
})
}
fn source_is_consumed_or_mutated(&self, ctx: &AnalysisContext, cloned_var: VarId) -> bool {
let dataflow = &ctx.dataflow_graph;
for &flow_id in dataflow.incoming(cloned_var) {
let Some(flow_data) = dataflow.flow(flow_id) else {
continue;
};
if flow_data.kind != FlowKind::Clone {
continue;
}
let Some(edge) = dataflow.edge(flow_id) else {
continue;
};
let source_var = edge.from;
for &out_flow_id in dataflow.outgoing(source_var) {
if let Some(out_flow) = dataflow.flow(out_flow_id) {
match out_flow.kind {
FlowKind::Move
| FlowKind::Argument
| FlowKind::MutBorrow
| FlowKind::Write
| FlowKind::Assign => return true,
_ => {}
}
}
}
}
false
}
}
impl Default for UnnecessaryClone {
fn default() -> Self {
Self::new()
}
}
impl PerformanceSuggest for UnnecessaryClone {
fn code(&self) -> &'static str {
"RP001"
}
fn default_severity(&self) -> LintSeverity {
LintSeverity::Warning
}
}
impl Suggest for UnnecessaryClone {
fn name(&self) -> &'static str {
"unnecessary-clone"
}
fn description(&self) -> &str {
"Detects clone() calls where ownership is not actually needed"
}
fn category(&self) -> SuggestCategory {
SuggestCategory::Performance
}
fn safety_level(&self) -> SafetyLevel {
SafetyLevel::Confirm }
fn priority_weight(&self) -> f32 {
1.2 }
fn detect(&self, ctx: &AnalysisContext, symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
let mut opportunities = Vec::new();
let mut next_id = 0u32;
let dataflow = &ctx.dataflow_graph;
let parent_symbols: Vec<SymbolId> = if symbols.is_empty() {
ctx.registry
.iter_by_kind(ryo_analysis::SymbolKind::Function)
.chain(ctx.registry.iter_by_kind(ryo_analysis::SymbolKind::Method))
.collect()
} else {
symbols.to_vec()
};
for (_flow_id, flow_data, edge) in dataflow.iter_flows() {
if flow_data.kind != FlowKind::Clone {
continue;
}
let cloned_var = edge.to;
let Some(var_data) = dataflow.var(cloned_var) else {
continue;
};
if !symbols.is_empty() && !parent_symbols.contains(&var_data.parent) {
continue;
}
if var_data.kind == VarKind::Temp {
continue;
}
let Some(analysis) = self.analyze_clone_necessity(ctx, cloned_var) else {
continue;
};
if analysis.confidence < self.min_confidence {
continue;
}
let Some(location) = SuggestLocation::from_context(ctx, var_data.parent) else {
continue;
};
let var_name = dataflow
.var_name(cloned_var)
.unwrap_or("<unknown>")
.to_string();
let message = format!(
"Unnecessary clone() for `{}`: {}",
var_name,
analysis.pattern.description()
);
let opp = self.create_performance_opportunity(
OpportunityId::new(next_id),
vec![var_data.parent],
location,
message,
PerformanceDetails {
current_pattern: format!("let {} = source.clone()", var_name),
suggested_pattern: analysis.suggestion,
confidence: analysis.confidence,
},
);
opportunities.push(opp);
next_id += 1;
}
opportunities
}
fn to_mutation_specs(
&self,
_ctx: &AnalysisContext,
_opportunity: &SuggestOpportunity,
) -> SuggestResult<Vec<MutationSpec>> {
Ok(Vec::new())
}
}
struct CloneAnalysis {
pattern: ClonePattern,
confidence: f32,
suggestion: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ClonePattern {
ImmediatelyDropped,
OnlySharedBorrow,
ArgumentNoEscape,
}
impl ClonePattern {
fn description(&self) -> &'static str {
match self {
Self::ImmediatelyDropped => "cloned value is never used",
Self::OnlySharedBorrow => "cloned value is only borrowed (not owned)",
Self::ArgumentNoEscape => "cloned value doesn't escape function scope",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clone_pattern_description() {
assert!(ClonePattern::ImmediatelyDropped
.description()
.contains("never used"));
assert!(ClonePattern::OnlySharedBorrow
.description()
.contains("borrowed"));
assert!(ClonePattern::ArgumentNoEscape
.description()
.contains("escape"));
}
#[test]
fn test_confidence_threshold() {
let rule = UnnecessaryClone::new().with_min_confidence(0.8);
assert!((rule.min_confidence - 0.8).abs() < f32::EPSILON);
let rule = UnnecessaryClone::new().with_min_confidence(1.5);
assert!((rule.min_confidence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_rule_metadata() {
let rule = UnnecessaryClone::new();
assert_eq!(rule.code(), "RP001");
assert_eq!(rule.name(), "unnecessary-clone");
assert_eq!(rule.category(), SuggestCategory::Performance);
assert_eq!(rule.safety_level(), SafetyLevel::Confirm);
}
}