use std::collections::{HashMap, HashSet};
use std::path::Path;
use crate::reverse_coverage::{reverse_coverage, PubFn, ReverseCoverageReport};
use crate::schema::Contract;
#[derive(Debug, Clone)]
pub struct InferredBinding {
pub function: PubFn,
pub contract_stem: String,
pub equation: String,
pub confidence: f64,
pub strategy: MatchStrategy,
}
#[derive(Debug, Clone)]
pub struct ContractSuggestion {
pub function: PubFn,
pub suggested_name: String,
pub suggested_tier: u8,
pub reason: String,
}
#[derive(Debug, Clone, Copy)]
pub enum MatchStrategy {
NameMatch,
ModuleMatch,
SignatureMatch,
}
impl std::fmt::Display for MatchStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NameMatch => write!(f, "name"),
Self::ModuleMatch => write!(f, "module"),
Self::SignatureMatch => write!(f, "signature"),
}
}
}
#[derive(Debug)]
pub struct InferResult {
pub matched: Vec<InferredBinding>,
pub suggestions: Vec<ContractSuggestion>,
pub coverage: ReverseCoverageReport,
}
pub fn infer(
crate_dir: &Path,
binding_path: &Path,
contracts: &[(String, &Contract)],
) -> InferResult {
let coverage = reverse_coverage(crate_dir, binding_path);
let mut eq_index: HashMap<String, (String, String)> = HashMap::new();
let mut eq_keywords: Vec<(String, String, Vec<String>)> = Vec::new();
for (stem, contract) in contracts {
for (eq_name, eq) in &contract.equations {
let normalized = normalize_name(eq_name);
eq_index.insert(normalized.clone(), (stem.clone(), eq_name.clone()));
let mut keywords = Vec::new();
keywords.extend(tokenize(&eq.formula));
if let Some(ref dom) = eq.domain {
keywords.extend(tokenize(dom));
}
keywords.push(normalized);
eq_keywords.push((stem.clone(), eq_name.clone(), keywords));
}
}
let bound_names: HashSet<String> = extract_bound_fn_names(binding_path);
let mut matched = Vec::new();
let mut suggestions = Vec::new();
for func in &coverage.unbound {
let fn_name = normalize_name(&func.path);
if is_trivial(&func.path) {
continue;
}
if let Some((stem, eq)) = eq_index.get(&fn_name) {
if !bound_names.contains(&fn_name) {
matched.push(InferredBinding {
function: func.clone(),
contract_stem: stem.clone(),
equation: eq.clone(),
confidence: 0.95,
strategy: MatchStrategy::NameMatch,
});
continue;
}
}
if let Some(best) = best_fuzzy_match(&fn_name, &eq_keywords) {
matched.push(InferredBinding {
function: func.clone(),
contract_stem: best.0,
equation: best.1,
confidence: best.2,
strategy: MatchStrategy::NameMatch,
});
continue;
}
let tier = infer_tier_from_path(&func.file);
let suggested_name = suggest_contract_name(&func.path);
if !suggested_name.is_empty() {
suggestions.push(ContractSuggestion {
function: func.clone(),
suggested_name,
suggested_tier: tier,
reason: format!("Tier {tier} function with no matching contract"),
});
}
}
matched.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
InferResult {
matched,
suggestions,
coverage,
}
}
fn normalize_name(name: &str) -> String {
name.to_lowercase()
.replace("::", "_")
.replace('-', "_")
.replace("_v1", "")
.replace("_kernel", "")
}
fn tokenize(s: &str) -> Vec<String> {
s.to_lowercase()
.split(|c: char| !c.is_alphanumeric() && c != '_')
.filter(|w| w.len() > 2)
.map(String::from)
.collect()
}
fn is_trivial(name: &str) -> bool {
let trivial = [
"new",
"default",
"from",
"into",
"as_ref",
"as_mut",
"len",
"is_empty",
"clone",
"fmt",
"display",
"debug",
"eq",
"ne",
"hash",
"cmp",
"partial_cmp",
"drop",
"deref",
"deref_mut",
"index",
"index_mut",
"with_",
"set_",
"get_",
];
let lower = name.to_lowercase();
trivial.iter().any(|t| lower == *t || lower.starts_with(t))
}
fn best_fuzzy_match(
fn_name: &str,
eq_keywords: &[(String, String, Vec<String>)],
) -> Option<(String, String, f64)> {
let mut best: Option<(String, String, f64)> = None;
for (stem, eq_name, keywords) in eq_keywords {
let eq_norm = normalize_name(eq_name);
let score = if fn_name.contains(&eq_norm) || eq_norm.contains(fn_name) {
0.85
} else {
let fn_tokens: HashSet<String> = fn_name
.split('_')
.filter(|w| w.len() > 2)
.map(str::to_lowercase)
.collect();
let keyword_set: HashSet<&str> = keywords.iter().map(String::as_str).collect();
let overlap = fn_tokens
.iter()
.filter(|t| keyword_set.contains(t.as_str()))
.count();
if overlap > 0 && !fn_tokens.is_empty() {
#[allow(clippy::cast_precision_loss)]
{
0.5 + 0.3 * (overlap as f64 / fn_tokens.len() as f64)
}
} else {
0.0
}
};
if score > 0.5 && !best.as_ref().is_some_and(|b| b.2 >= score) {
best = Some((stem.clone(), eq_name.clone(), score));
}
}
best
}
fn extract_bound_fn_names(binding_path: &Path) -> HashSet<String> {
let mut names = HashSet::new();
if let Ok(content) = std::fs::read_to_string(binding_path) {
for line in content.lines() {
let trimmed = line.trim();
let func_line = trimmed.strip_prefix("- ").unwrap_or(trimmed);
if let Some(rest) = func_line.strip_prefix("function:") {
let fname = rest.trim().trim_matches('"').trim();
let short = fname.rsplit("::").next().unwrap_or(fname);
names.insert(normalize_name(short));
}
}
}
names
}
fn infer_tier_from_path(path: &str) -> u8 {
let p = path.to_lowercase();
if p.contains("kernel") || p.contains("simd") || p.contains("avx") || p.contains("neon") {
1 } else if p.contains("attention") || p.contains("transformer") {
2 } else if p.contains("cache") || p.contains("scheduler") || p.contains("dispatch") {
3 } else if p.contains("train") || p.contains("optim") || p.contains("grad") {
4 } else if p.contains("ml") || p.contains("cluster") || p.contains("classify") {
5 } else {
3 }
}
fn suggest_contract_name(fn_name: &str) -> String {
let clean = fn_name.to_lowercase().replace("::", "-").replace('_', "-");
if clean.len() < 3 || is_trivial(fn_name) {
return String::new();
}
format!("{clean}-v1")
}
pub fn format_binding_entry(inferred: &InferredBinding) -> String {
format!(
" - contract: {}.yaml\n equation: {}\n module_path: ~\n function: \"{}\"\n status: not_implemented\n notes: \"Auto-inferred ({}, confidence {:.0}%)\"",
inferred.contract_stem,
inferred.equation,
inferred.function.path,
inferred.strategy,
inferred.confidence * 100.0,
)
}
pub fn format_contract_stub(suggestion: &ContractSuggestion) -> String {
format!(
r#"metadata:
version: "1.0.0"
description: "Auto-suggested contract for {fn_name}"
references:
- "pv infer — auto-generated"
equations:
{eq_name}:
formula: "TODO: define equation"
domain: "TODO: define domain"
proof_obligations:
- type: invariant
property: "TODO: define invariant"
falsification_tests:
- id: FALSIFY-{prefix}-001
rule: "TODO"
prediction: "TODO"
if_fails: "TODO"
kani_harnesses:
- id: KANI-{prefix}-001
obligation: "TODO"
bound: 16
strategy: bounded_int
solver: cadical
"#,
fn_name = suggestion.function.path,
eq_name = suggestion.function.path.to_lowercase().replace("::", "_"),
prefix = suggestion
.function
.path
.to_uppercase()
.chars()
.take(3)
.collect::<String>(),
)
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]
use super::*;
#[test]
fn test_normalize_name() {
assert_eq!(normalize_name("softmax_kernel_v1"), "softmax");
assert_eq!(normalize_name("RMSNorm"), "rmsnorm");
assert_eq!(normalize_name("ssm-scan"), "ssm_scan");
}
#[test]
fn test_is_trivial() {
assert!(is_trivial("new"));
assert!(is_trivial("default"));
assert!(is_trivial("with_capacity"));
assert!(is_trivial("set_threshold"));
assert!(!is_trivial("softmax"));
assert!(!is_trivial("rmsnorm"));
}
#[test]
fn test_infer_tier() {
assert_eq!(infer_tier_from_path("src/kernels/softmax.rs"), 1);
assert_eq!(infer_tier_from_path("src/nn/transformer/attention.rs"), 2);
assert_eq!(infer_tier_from_path("src/scheduler/mod.rs"), 3);
assert_eq!(infer_tier_from_path("src/train/optimizer.rs"), 4);
}
#[test]
fn test_suggest_contract_name() {
assert_eq!(suggest_contract_name("softmax"), "softmax-v1");
assert_eq!(suggest_contract_name("batch_norm"), "batch-norm-v1");
assert_eq!(suggest_contract_name("new"), ""); }
#[test]
fn test_tokenize() {
let tokens = tokenize("exp(x_i - max(x)) / sum");
assert!(tokens.contains(&"exp".to_string()));
assert!(tokens.contains(&"max".to_string()));
assert!(tokens.contains(&"sum".to_string()));
}
#[test]
fn test_fuzzy_match() {
let eq_keywords = vec![
(
"softmax-kernel-v1".into(),
"softmax".into(),
vec!["softmax".into(), "exp".into(), "sum".into()],
),
(
"rmsnorm-kernel-v1".into(),
"rmsnorm".into(),
vec!["rmsnorm".into(), "sqrt".into(), "mean".into()],
),
];
let result = best_fuzzy_match("softmax_avx2", &eq_keywords);
assert!(result.is_some());
let (stem, _, conf) = result.unwrap();
assert_eq!(stem, "softmax-kernel-v1");
assert!(conf > 0.8);
let result = best_fuzzy_match("parse_config", &eq_keywords);
assert!(result.is_none());
}
#[test]
fn test_match_strategy_display() {
assert_eq!(MatchStrategy::NameMatch.to_string(), "name");
assert_eq!(MatchStrategy::ModuleMatch.to_string(), "module");
assert_eq!(MatchStrategy::SignatureMatch.to_string(), "signature");
}
#[test]
fn test_normalize_name_chained_replacements() {
assert_eq!(
normalize_name("aprender::nn::softmax"),
"aprender_nn_softmax"
);
assert_eq!(normalize_name("silu-kernel-v1"), "silu");
assert_eq!(normalize_name("gelu_kernel_v1"), "gelu");
assert_eq!(normalize_name("layernorm"), "layernorm");
assert_eq!(normalize_name("model_v1_extra_v1"), "model_extra");
}
#[test]
fn test_tokenize_filters_short_words() {
let tokens = tokenize("a + bb + ccc");
assert!(!tokens.contains(&"a".to_string()));
assert!(!tokens.contains(&"bb".to_string()));
assert!(tokens.contains(&"ccc".to_string()));
}
#[test]
fn test_tokenize_empty() {
let tokens = tokenize("");
assert!(tokens.is_empty());
}
#[test]
fn test_tokenize_all_short() {
let tokens = tokenize("x + y = z");
assert!(tokens.is_empty());
}
#[test]
fn test_is_trivial_prefix_matches() {
assert!(is_trivial("get_value"));
assert!(is_trivial("with_config"));
assert!(is_trivial("set_mode"));
assert!(is_trivial("as_ref"));
assert!(is_trivial("as_mut"));
assert!(is_trivial("len"));
assert!(is_trivial("is_empty"));
assert!(is_trivial("clone"));
assert!(is_trivial("fmt"));
assert!(is_trivial("display"));
assert!(is_trivial("debug"));
assert!(is_trivial("eq"));
assert!(is_trivial("ne"));
assert!(is_trivial("hash"));
assert!(is_trivial("cmp"));
assert!(is_trivial("partial_cmp"));
assert!(is_trivial("drop"));
assert!(is_trivial("deref"));
assert!(is_trivial("deref_mut"));
assert!(is_trivial("index"));
assert!(is_trivial("index_mut"));
assert!(is_trivial("from"));
assert!(is_trivial("into"));
}
#[test]
fn test_is_trivial_case_insensitive() {
assert!(is_trivial("New"));
assert!(is_trivial("DEFAULT"));
assert!(is_trivial("Clone"));
}
#[test]
fn test_is_trivial_non_trivial_names() {
assert!(!is_trivial("forward"));
assert!(!is_trivial("compute_attention"));
assert!(!is_trivial("matmul"));
assert!(!is_trivial("encode"));
}
#[test]
fn test_infer_tier_simd_paths() {
assert_eq!(infer_tier_from_path("src/simd/avx2.rs"), 1);
assert_eq!(infer_tier_from_path("src/kernels/neon_impl.rs"), 1);
assert_eq!(infer_tier_from_path("src/avx512/softmax.rs"), 1);
}
#[test]
fn test_infer_tier_transformer() {
assert_eq!(infer_tier_from_path("src/transformer/multi_head.rs"), 2);
}
#[test]
fn test_infer_tier_cache_and_dispatch() {
assert_eq!(infer_tier_from_path("src/cache/kv_store.rs"), 3);
assert_eq!(infer_tier_from_path("src/dispatch/router.rs"), 3);
}
#[test]
fn test_infer_tier_training() {
assert_eq!(infer_tier_from_path("src/optimizer/adam.rs"), 4);
assert_eq!(infer_tier_from_path("src/grad/backward.rs"), 4);
}
#[test]
fn test_infer_tier_classical_ml() {
assert_eq!(infer_tier_from_path("src/ml/kmeans.rs"), 5);
assert_eq!(infer_tier_from_path("src/cluster/dbscan.rs"), 5);
assert_eq!(infer_tier_from_path("src/classify/svm.rs"), 5);
}
#[test]
fn test_infer_tier_default() {
assert_eq!(infer_tier_from_path("src/utils/helpers.rs"), 3);
assert_eq!(infer_tier_from_path("src/lib.rs"), 3);
}
#[test]
fn test_suggest_contract_name_short() {
assert_eq!(suggest_contract_name("ab"), "");
}
#[test]
fn test_suggest_contract_name_with_colons() {
assert_eq!(
suggest_contract_name("aprender::nn::softmax"),
"aprender-nn-softmax-v1"
);
}
#[test]
fn test_suggest_contract_name_with_underscores() {
assert_eq!(suggest_contract_name("layer_norm"), "layer-norm-v1");
}
#[test]
fn test_extract_bound_fn_names_empty_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("binding.yaml");
std::fs::write(&path, "").unwrap();
let names = extract_bound_fn_names(&path);
assert!(names.is_empty());
}
#[test]
fn test_extract_bound_fn_names_nonexistent_file() {
let names = extract_bound_fn_names(Path::new("/nonexistent/binding.yaml"));
assert!(names.is_empty());
}
#[test]
fn test_extract_bound_fn_names_parses_functions() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("binding.yaml");
let content = r#"bindings:
- function: "aprender::nn::softmax"
contract: softmax-kernel-v1.yaml
equation: softmax
status: implemented
- function: "aprender::nn::rmsnorm"
contract: rmsnorm-kernel-v1.yaml
equation: rmsnorm
status: not_implemented
"#;
std::fs::write(&path, content).unwrap();
let names = extract_bound_fn_names(&path);
assert!(names.contains(&normalize_name("softmax")));
assert!(names.contains(&normalize_name("rmsnorm")));
}
#[test]
fn test_extract_bound_fn_names_with_quotes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("binding.yaml");
let content = r#" - function: "crate::module::func_name"
- function: another_func
"#;
std::fs::write(&path, content).unwrap();
let names = extract_bound_fn_names(&path);
assert!(names.contains(&normalize_name("func_name")));
assert!(names.contains(&normalize_name("another_func")));
}
#[test]
fn test_format_binding_entry() {
let binding = InferredBinding {
function: PubFn {
path: "aprender::nn::softmax".to_string(),
file: "src/nn/softmax.rs".to_string(),
line: 42,
has_contract_macro: false,
feature_gate: None,
},
contract_stem: "softmax-kernel-v1".to_string(),
equation: "softmax".to_string(),
confidence: 0.95,
strategy: MatchStrategy::NameMatch,
};
let entry = format_binding_entry(&binding);
assert!(entry.contains("softmax-kernel-v1.yaml"));
assert!(entry.contains("equation: softmax"));
assert!(entry.contains("aprender::nn::softmax"));
assert!(entry.contains("not_implemented"));
assert!(entry.contains("Auto-inferred (name, confidence 95%)"));
}
#[test]
fn test_format_binding_entry_module_match() {
let binding = InferredBinding {
function: PubFn {
path: "crate::attention::mha".to_string(),
file: "src/attention/mha.rs".to_string(),
line: 10,
has_contract_macro: false,
feature_gate: None,
},
contract_stem: "attention-v1".to_string(),
equation: "multi_head_attention".to_string(),
confidence: 0.70,
strategy: MatchStrategy::ModuleMatch,
};
let entry = format_binding_entry(&binding);
assert!(entry.contains("Auto-inferred (module, confidence 70%)"));
}
#[test]
fn test_format_binding_entry_signature_match() {
let binding = InferredBinding {
function: PubFn {
path: "crate::ops::elementwise".to_string(),
file: "src/ops.rs".to_string(),
line: 5,
has_contract_macro: false,
feature_gate: None,
},
contract_stem: "elementwise-v1".to_string(),
equation: "elementwise_op".to_string(),
confidence: 0.60,
strategy: MatchStrategy::SignatureMatch,
};
let entry = format_binding_entry(&binding);
assert!(entry.contains("Auto-inferred (signature, confidence 60%)"));
}
#[test]
fn test_format_contract_stub() {
let suggestion = ContractSuggestion {
function: PubFn {
path: "aprender::nn::gelu".to_string(),
file: "src/nn/gelu.rs".to_string(),
line: 1,
has_contract_macro: false,
feature_gate: None,
},
suggested_name: "gelu-v1".to_string(),
suggested_tier: 1,
reason: "Tier 1 function with no matching contract".to_string(),
};
let stub = format_contract_stub(&suggestion);
assert!(stub.contains("aprender::nn::gelu"));
assert!(stub.contains("aprender_nn_gelu"));
assert!(stub.contains("FALSIFY-APR-001"));
assert!(stub.contains("KANI-APR-001"));
assert!(stub.contains("metadata:"));
assert!(stub.contains("equations:"));
assert!(stub.contains("proof_obligations:"));
assert!(stub.contains("falsification_tests:"));
assert!(stub.contains("kani_harnesses:"));
assert!(stub.contains("pv infer"));
}
#[test]
fn test_format_contract_stub_short_prefix() {
let suggestion = ContractSuggestion {
function: PubFn {
path: "ab".to_string(),
file: "src/ab.rs".to_string(),
line: 1,
has_contract_macro: false,
feature_gate: None,
},
suggested_name: "ab-v1".to_string(),
suggested_tier: 3,
reason: "test".to_string(),
};
let stub = format_contract_stub(&suggestion);
assert!(stub.contains("FALSIFY-AB-001"));
assert!(stub.contains("KANI-AB-001"));
}
#[test]
fn test_fuzzy_match_keyword_overlap() {
let eq_keywords = vec![(
"attention-v1".to_string(),
"attention_score".to_string(),
vec![
"attention".to_string(),
"score".to_string(),
"softmax".to_string(),
],
)];
let result = best_fuzzy_match("compute_attention_heads", &eq_keywords);
assert!(result.is_some());
let (stem, eq, conf) = result.unwrap();
assert_eq!(stem, "attention-v1");
assert_eq!(eq, "attention_score");
assert!(conf > 0.5);
assert!(conf <= 0.85);
}
#[test]
fn test_fuzzy_match_eq_contains_fn() {
let eq_keywords = vec![(
"layernorm-v1".to_string(),
"layer_normalization".to_string(),
vec!["layer".to_string(), "normalization".to_string()],
)];
let result = best_fuzzy_match("norm", &eq_keywords);
assert!(result.is_some());
let (_, _, conf) = result.unwrap();
assert!((conf - 0.85).abs() < f64::EPSILON);
}
#[test]
fn test_fuzzy_match_no_overlap_returns_none() {
let eq_keywords = vec![(
"softmax-v1".to_string(),
"softmax".to_string(),
vec!["softmax".to_string(), "exp".to_string()],
)];
let result = best_fuzzy_match("completely_unrelated_function", &eq_keywords);
assert!(result.is_none());
}
#[test]
fn test_fuzzy_match_picks_highest_confidence() {
let eq_keywords = vec![
(
"stem-a".to_string(),
"alpha".to_string(),
vec!["alpha".to_string(), "beta".to_string()],
),
(
"stem-b".to_string(),
"alpha_beta".to_string(),
vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()],
),
];
let result = best_fuzzy_match("alpha", &eq_keywords);
assert!(result.is_some());
let (stem, _, conf) = result.unwrap();
assert!((conf - 0.85).abs() < f64::EPSILON);
assert_eq!(stem, "stem-a");
}
#[test]
fn test_fuzzy_match_empty_keywords() {
let eq_keywords: Vec<(String, String, Vec<String>)> = vec![];
let result = best_fuzzy_match("anything", &eq_keywords);
assert!(result.is_none());
}
#[test]
fn test_infer_with_temp_crate() {
use crate::schema::parse_contract_str;
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(
src.join("lib.rs"),
"pub fn my_softmax(x: &[f32]) -> Vec<f32> { vec![] }\npub fn new() -> () { () }\n",
)
.unwrap();
let binding_path = dir.path().join("binding.yaml");
std::fs::write(
&binding_path,
"version: \"1.0.0\"\ntarget_crate: test\nbindings: []\n",
)
.unwrap();
let contract = parse_contract_str(
r#"
metadata:
version: "1.0.0"
description: "Softmax kernel"
equations:
softmax:
formula: "σ(x)_i = exp(x_i) / Σ exp(x_j)"
domain: "x ∈ ℝ^n"
falsification_tests: []
"#,
)
.unwrap();
let contracts = vec![("softmax-kernel-v1".to_string(), &contract)];
let contracts_ref: Vec<(String, &crate::schema::Contract)> =
contracts.iter().map(|(s, c)| (s.clone(), *c)).collect();
let result = infer(dir.path(), &binding_path, &contracts_ref);
let _ = result.coverage.total_pub_fns;
}
#[test]
fn test_infer_result_sorting() {
let result = InferResult {
matched: vec![
InferredBinding {
function: PubFn {
path: "low_conf".to_string(),
file: "a.rs".to_string(),
line: 1,
has_contract_macro: false,
feature_gate: None,
},
contract_stem: "a-v1".to_string(),
equation: "a".to_string(),
confidence: 0.5,
strategy: MatchStrategy::NameMatch,
},
InferredBinding {
function: PubFn {
path: "high_conf".to_string(),
file: "b.rs".to_string(),
line: 1,
has_contract_macro: false,
feature_gate: None,
},
contract_stem: "b-v1".to_string(),
equation: "b".to_string(),
confidence: 0.95,
strategy: MatchStrategy::NameMatch,
},
],
suggestions: vec![],
coverage: ReverseCoverageReport {
total_pub_fns: 0,
bound_fns: 0,
annotated_fns: 0,
exempt_fns: 0,
unbound: vec![],
coverage_pct: 0.0,
},
};
assert_eq!(result.matched.len(), 2);
assert_eq!(result.suggestions.len(), 0);
}
#[test]
fn test_inferred_binding_clone_and_debug() {
let binding = InferredBinding {
function: PubFn {
path: "test".to_string(),
file: "test.rs".to_string(),
line: 1,
has_contract_macro: false,
feature_gate: None,
},
contract_stem: "test-v1".to_string(),
equation: "test_eq".to_string(),
confidence: 0.9,
strategy: MatchStrategy::NameMatch,
};
let cloned = binding.clone();
assert!((cloned.confidence - 0.9_f64).abs() < f64::EPSILON);
let debug = format!("{cloned:?}");
assert!(!debug.is_empty());
}
#[test]
fn test_contract_suggestion_clone_and_debug() {
let suggestion = ContractSuggestion {
function: PubFn {
path: "my_func".to_string(),
file: "src/lib.rs".to_string(),
line: 42,
has_contract_macro: false,
feature_gate: None,
},
suggested_name: "my-func-v1".to_string(),
suggested_tier: 2,
reason: "test reason".to_string(),
};
let cloned = suggestion.clone();
assert_eq!(cloned.suggested_tier, 2);
let debug = format!("{cloned:?}");
assert!(debug.contains("my_func"));
}
#[test]
fn test_match_strategy_copy() {
let s = MatchStrategy::SignatureMatch;
let copied = s;
assert_eq!(format!("{copied}"), "signature");
assert_eq!(format!("{s}"), "signature");
}
}