use ryo_source::pure::PureFile;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DetectCategory {
Creational,
Structural,
Behavioural,
Performance,
Safety,
Clippy,
Refactor,
}
impl DetectCategory {
pub fn as_str(&self) -> &'static str {
match self {
DetectCategory::Creational => "creational",
DetectCategory::Structural => "structural",
DetectCategory::Behavioural => "behavioural",
DetectCategory::Performance => "performance",
DetectCategory::Safety => "safety",
DetectCategory::Clippy => "clippy",
DetectCategory::Refactor => "refactor",
}
}
pub fn short_code(&self) -> &'static str {
match self {
DetectCategory::Creational => "C",
DetectCategory::Structural => "S",
DetectCategory::Behavioural => "B",
DetectCategory::Performance => "P",
DetectCategory::Safety => "Y",
DetectCategory::Clippy => "L",
DetectCategory::Refactor => "R",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectLocation {
pub item_kind: String,
pub item_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_path: Option<String>,
}
impl DetectLocation {
pub fn new(kind: impl Into<String>, name: impl Into<String>) -> Self {
Self {
item_kind: kind.into(),
item_name: name.into(),
symbol_path: None,
}
}
pub fn struct_item(name: impl Into<String>) -> Self {
Self::new("struct", name)
}
pub fn impl_item(name: impl Into<String>) -> Self {
Self::new("impl", name)
}
pub fn fn_item(name: impl Into<String>) -> Self {
Self::new("fn", name)
}
pub fn with_symbol_path(mut self, path: impl Into<String>) -> Self {
self.symbol_path = Some(path.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DetectOperation {
Generate,
Refactor,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectOpportunity {
pub location: DetectLocation,
pub operations: Vec<DetectOperation>,
pub suggestion: String,
pub confidence: f32,
pub context: Option<String>,
}
impl DetectOpportunity {
pub fn new(location: DetectLocation, suggestion: impl Into<String>) -> Self {
Self {
location,
operations: vec![DetectOperation::Generate],
suggestion: suggestion.into(),
confidence: 1.0,
context: None,
}
}
pub fn with_operations(mut self, ops: Vec<DetectOperation>) -> Self {
self.operations = ops;
self
}
pub fn with_confidence(mut self, confidence: f32) -> Self {
self.confidence = confidence.clamp(0.0, 1.0);
self
}
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
pub fn can_generate(&self) -> bool {
self.operations.contains(&DetectOperation::Generate)
}
pub fn can_refactor(&self) -> bool {
self.operations.contains(&DetectOperation::Refactor)
}
}
pub trait Detect: Send + Sync {
fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity>;
fn category(&self) -> DetectCategory;
fn detect_name(&self) -> &'static str;
fn detect_description(&self) -> &str {
""
}
}
#[derive(Default)]
pub struct DetectRegistry {
detectors: Vec<Box<dyn Detect>>,
}
impl DetectRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<D: Detect + 'static>(&mut self, detector: D) {
self.detectors.push(Box::new(detector));
}
pub fn all(&self) -> &[Box<dyn Detect>] {
&self.detectors
}
pub fn find(&self, name: &str) -> Option<&dyn Detect> {
self.detectors
.iter()
.find(|d| d.detect_name().eq_ignore_ascii_case(name))
.map(|d| d.as_ref())
}
pub fn by_category(&self, category: DetectCategory) -> Vec<&dyn Detect> {
self.detectors
.iter()
.filter(|d| d.category() == category)
.map(|d| d.as_ref())
.collect()
}
pub fn detect_all(&self, file: &PureFile) -> Vec<(&dyn Detect, DetectOpportunity)> {
self.detectors
.iter()
.flat_map(|d| d.detect(file).into_iter().map(move |opp| (d.as_ref(), opp)))
.collect()
}
}
pub fn create_default_registry() -> DetectRegistry {
use super::{
DefaultMutation, DeriveDefaultMutation, LockScopeMutation, UseAtomicMutation,
UseRwLockMutation,
};
let mut registry = DetectRegistry::new();
registry.register(DefaultMutation::new());
registry.register(DeriveDefaultMutation::new());
registry.register(UseAtomicMutation::new());
registry.register(UseRwLockMutation::new());
registry.register(LockScopeMutation::new());
registry
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_location() {
let loc = DetectLocation::struct_item("Config");
assert_eq!(loc.item_kind, "struct");
assert_eq!(loc.item_name, "Config");
}
#[test]
fn test_detect_opportunity() {
let opp = DetectOpportunity::new(
DetectLocation::struct_item("Config"),
"Generate Builder pattern",
)
.with_operations(vec![DetectOperation::Generate])
.with_confidence(0.9);
assert!(opp.can_generate());
assert!(!opp.can_refactor());
assert_eq!(opp.confidence, 0.9);
}
#[test]
fn test_detect_category() {
assert_eq!(DetectCategory::Creational.as_str(), "creational");
assert_eq!(DetectCategory::Clippy.as_str(), "clippy");
}
#[test]
fn test_all_detectors_are_registered() {
use super::super::{
DefaultMutation, DeriveDefaultMutation, LockScopeMutation, UseAtomicMutation,
UseRwLockMutation,
};
use std::collections::HashSet;
let registry = create_default_registry();
let registered_names: HashSet<&str> =
registry.all().iter().map(|d| d.detect_name()).collect();
let expected_detectors: Vec<(&str, Box<dyn Detect>)> = vec![
("DefaultMutation", Box::new(DefaultMutation::new())),
(
"DeriveDefaultMutation",
Box::new(DeriveDefaultMutation::new()),
),
("UseAtomicMutation", Box::new(UseAtomicMutation::new())),
("UseRwLockMutation", Box::new(UseRwLockMutation::new())),
("LockScopeMutation", Box::new(LockScopeMutation::new())),
];
for (label, detector) in expected_detectors {
let name = detector.detect_name();
assert!(
registered_names.contains(name),
"{} (detect_name='{}') is NOT registered in create_default_registry(). \
Add: registry.register({}::new());",
label,
name,
label
);
}
assert_eq!(
registry.all().len(),
5,
"Expected 5 detectors registered, found {}",
registry.all().len()
);
}
}