diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..6da60a5 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,405 @@ +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::path::PathBuf; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FileLocation { + pub path: PathBuf, + pub line: usize, + pub column: usize, + pub end_line: Option, + pub end_column: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Priority { + Critical, + High, + Medium, + Low, +} + +impl Priority { + pub fn from_keyword(keyword: &str) -> Self { + match keyword.to_uppercase().as_str() { + "FIXME" | "BUG" | "ERROR" => Priority::Critical, + "XXX" | "URGENT" | "CRITICAL" => Priority::High, + "TODO" | "TEMP" | "PERF" => Priority::Medium, + "HACK" | "NOTE" | "REFACTOR" | "XXX" => Priority::Low, + _ => Priority::Medium, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Priority::Critical => "Critical", + Priority::High => "High", + Priority::Medium => "Medium", + Priority::Low => "Low", + } + } +} + +impl PartialOrd for Priority { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Priority { + fn cmp(&self, other: &Self) -> Ordering { + fn rank(p: &Priority) -> u8 { + match p { + Priority::Critical => 4, + Priority::High => 3, + Priority::Medium => 2, + Priority::Low => 1, + } + } + rank(self).cmp(&rank(other)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CommentType { + SingleLine, + MultiLine, + DocBlock, + Shebang, + Comment, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TechDebtItem { + pub id: String, + pub keyword: String, + pub comment_type: CommentType, + pub content: String, + pub location: FileLocation, + pub priority: Priority, + pub complexity_score: u8, + pub context_before: Option, + pub context_after: Option, + pub metadata: ItemMetadata, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemMetadata { + pub language: String, + pub is_question: bool, + pub has_exclamation: bool, + pub word_count: usize, + pub created_at: Option, + pub author: Option, +} + +impl TechDebtItem { + pub fn new( + keyword: String, + content: String, + location: FileLocation, + language: String, + comment_type: CommentType, + ) -> Self { + let complexity_score = calculate_complexity(&content); + let is_question = content.contains('?'); + let has_exclamation = content.contains('!'); + let word_count = content.split_whitespace().count(); + + let priority = Priority::from_keyword(&keyword); + + let id = format!( + "{:?}-{:?}-{}", + location.path, + location.line, + keyword + ); + + Self { + id, + keyword, + comment_type, + content, + location, + priority, + complexity_score, + context_before: None, + context_after: None, + metadata: ItemMetadata { + language, + is_question, + has_exclamation, + word_count, + created_at: None, + author: None, + }, + } + } +} + +fn calculate_complexity(content: &str) -> u8 { + let mut score = 1; + + let question_count = content.matches('?').count(); + let exclamation_count = content.matches('!').count(); + let word_count = content.split_whitespace().count(); + let uppercase_count = content.chars().filter(|c| c.is_uppercase()).count(); + + score += (question_count * 2) as u8; + score += exclamation_count as u8; + score += (word_count / 10) as u8; + score += (uppercase_count / 5) as u8; + + if content.len() > 200 { + score += 2; + } + if content.len() > 500 { + score += 3; + } + + if content.contains("HACK") || content.contains("WORKAROUND") { + score += 2; + } + + std::cmp::min(score, 10) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalysisSummary { + pub total_items: usize, + pub by_priority: ByPriority, + pub by_language: ByLanguage, + pub complexity_distribution: ComplexityDistribution, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ByPriority { + pub critical: usize, + pub high: usize, + pub medium: usize, + pub low: usize, +} + +impl ByPriority { + pub fn from_items(items: &[TechDebtItem]) -> Self { + let mut critical = 0; + let mut high = 0; + let mut medium = 0; + let mut low = 0; + + for item in items { + match item.priority { + Priority::Critical => critical += 1, + Priority::High => high += 1, + Priority::Medium => medium += 1, + Priority::Low => low += 1, + } + } + + Self { + critical, + high, + medium, + low, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ByLanguage { + pub items: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LanguageCount { + pub language: String, + pub count: usize, +} + +impl ByLanguage { + pub fn from_items(items: &[TechDebtItem]) -> Self { + let mut counts: std::collections::HashMap = + std::collections::HashMap::new(); + + for item in items { + *counts + .entry(item.metadata.language.clone()) + .or_insert(0) += 1; + } + + let mut items_vec: Vec = counts + .into_iter() + .map(|(lang, count)| LanguageCount { lang, count }) + .collect(); + + items_vec.sort_by(|a, b| b.count.cmp(&a.count)); + + Self { items: items_vec } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplexityDistribution { + pub low: usize, + pub medium: usize, + pub high: usize, + pub critical: usize, +} + +impl ComplexityDistribution { + pub fn from_items(items: &[TechDebtItem]) -> Self { + let mut low = 0; + let mut medium = 0; + let mut high = 0; + let mut critical = 0; + + for item in items { + match item.complexity_score { + 1..=3 => low += 1, + 4..=6 => medium += 1, + 7..=8 => high += 1, + 9..=10 => critical += 1, + _ => {} + } + } + + Self { + low, + medium, + high, + critical, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub patterns: Vec, + pub languages: Vec, + pub ignore: Vec, + pub extensions: Vec, + pub complexity: ComplexityConfig, + pub export: ExportConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + patterns: vec![ + PatternConfig { + keyword: "FIXME".to_string(), + priority: Priority::Critical, + regex: false, + }, + PatternConfig { + keyword: "TODO".to_string(), + priority: Priority::Medium, + regex: false, + }, + PatternConfig { + keyword: "HACK".to_string(), + priority: Priority::Low, + regex: false, + }, + PatternConfig { + keyword: "BUG".to_string(), + priority: Priority::Critical, + regex: false, + }, + PatternConfig { + keyword: "XXX".to_string(), + priority: Priority::High, + regex: false, + }, + PatternConfig { + keyword: "NOTE".to_string(), + priority: Priority::Low, + regex: false, + }, + ], + languages: vec![ + "javascript".to_string(), + "typescript".to_string(), + "python".to_string(), + "rust".to_string(), + "go".to_string(), + "java".to_string(), + "c".to_string(), + "cpp".to_string(), + "ruby".to_string(), + ], + ignore: vec![ + "node_modules/**".to_string(), + "target/**".to_string(), + ".git/**".to_string(), + "vendor/**".to_string(), + "dist/**".to_string(), + "build/**".to_string(), + ], + extensions: vec![ + ".js".to_string(), + ".ts".to_string(), + ".jsx".to_string(), + ".tsx".to_string(), + ".py".to_string(), + ".rs".to_string(), + ".go".to_string(), + ".java".to_string(), + ".c".to_string(), + ".cpp".to_string(), + ".h".to_string(), + ".hpp".to_string(), + ".rb".to_string(), + ".md".to_string(), + ], + complexity: ComplexityConfig::default(), + export: ExportConfig::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PatternConfig { + pub keyword: String, + pub priority: Priority, + pub regex: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplexityConfig { + pub enabled: bool, + pub max_comment_length: usize, + pub question_weight: u8, + pub exclamation_weight: u8, +} + +impl Default for ComplexityConfig { + fn default() -> Self { + Self { + enabled: true, + max_comment_length: 500, + question_weight: 2, + exclamation_weight: 1, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportConfig { + pub include_metadata: bool, + pub include_context: bool, +} + +impl Default for ExportConfig { + fn default() -> Self { + Self { + include_metadata: true, + include_context: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GroupBy { + pub field: String, +}