syncable_cli/analyzer/frameworks/
mod.rs

1pub mod rust;
2pub mod javascript;
3pub mod python;
4pub mod go;
5pub mod java;
6
7use crate::analyzer::{DetectedTechnology, DetectedLanguage};
8use crate::error::Result;
9use std::collections::HashMap;
10
11/// Common interface for language-specific framework detection
12pub trait LanguageFrameworkDetector {
13    /// Detect frameworks for a specific language
14    fn detect_frameworks(&self, language: &DetectedLanguage) -> Result<Vec<DetectedTechnology>>;
15    
16    /// Get the supported language name(s) for this detector
17    fn supported_languages(&self) -> Vec<&'static str>;
18}
19
20/// Technology detection rules with proper classification and relationships
21#[derive(Clone, Debug)]
22pub struct TechnologyRule {
23    pub name: String,
24    pub category: crate::analyzer::TechnologyCategory,
25    pub confidence: f32,
26    pub dependency_patterns: Vec<String>,
27    /// Dependencies this technology requires (e.g., Next.js requires React)
28    pub requires: Vec<String>,
29    /// Technologies that conflict with this one (mutually exclusive)
30    pub conflicts_with: Vec<String>,
31    /// Whether this technology typically drives the architecture
32    pub is_primary_indicator: bool,
33    /// Alternative names for this technology
34    pub alternative_names: Vec<String>,
35    /// File indicators that can help identify this technology
36    pub file_indicators: Vec<String>,
37}
38
39/// Shared utilities for framework detection across languages
40pub struct FrameworkDetectionUtils;
41
42impl FrameworkDetectionUtils {
43    /// Generic technology detection based on dependency patterns
44    pub fn detect_technologies_by_dependencies(
45        rules: &[TechnologyRule],
46        dependencies: &[String],
47        base_confidence: f32,
48    ) -> Vec<DetectedTechnology> {
49        let mut technologies = Vec::new();
50        
51        // Debug logging for Tanstack Start detection
52        let tanstack_deps: Vec<_> = dependencies.iter()
53            .filter(|dep| dep.contains("tanstack") || dep.contains("vinxi"))
54            .collect();
55        if !tanstack_deps.is_empty() {
56            log::debug!("Found potential Tanstack dependencies: {:?}", tanstack_deps);
57        }
58        
59        for rule in rules {
60            let mut matches = 0;
61            let total_patterns = rule.dependency_patterns.len();
62            
63            if total_patterns == 0 {
64                continue;
65            }
66            
67            for pattern in &rule.dependency_patterns {
68                let matching_deps: Vec<_> = dependencies.iter()
69                    .filter(|dep| Self::matches_pattern(dep, pattern))
70                    .collect();
71                    
72                if !matching_deps.is_empty() {
73                    matches += 1;
74                    
75                    // Debug logging for Tanstack Start specifically
76                    if rule.name.contains("Tanstack") {
77                        log::debug!("Tanstack Start: Pattern '{}' matched dependencies: {:?}", pattern, matching_deps);
78                    }
79                }
80            }
81            
82            // Calculate confidence based on pattern matches and base language confidence
83            if matches > 0 {
84                let pattern_confidence = matches as f32 / total_patterns as f32;
85                // Use additive approach instead of multiplicative to avoid extremely low scores
86                // Base confidence provides a floor, pattern confidence provides the scaling
87                let final_confidence = (rule.confidence * pattern_confidence + base_confidence * 0.1).min(1.0);
88                
89                // Debug logging for Tanstack Start detection
90                if rule.name.contains("Tanstack") {
91                    log::debug!("Tanstack Start detected with {} matches out of {} patterns, confidence: {:.2}", 
92                              matches, total_patterns, final_confidence);
93                }
94                
95                technologies.push(DetectedTechnology {
96                    name: rule.name.clone(),
97                    version: None, // TODO: Extract version from dependencies
98                    category: rule.category.clone(),
99                    confidence: final_confidence,
100                    requires: rule.requires.clone(),
101                    conflicts_with: rule.conflicts_with.clone(),
102                    is_primary: rule.is_primary_indicator,
103                    file_indicators: rule.file_indicators.clone(),
104                });
105            } else if rule.name.contains("Tanstack") {
106                // Debug logging when Tanstack Start is not detected
107                log::debug!("Tanstack Start not detected - no patterns matched. Available dependencies: {:?}", 
108                          dependencies.iter().take(10).collect::<Vec<_>>());
109            }
110        }
111        
112        technologies
113    }
114
115    /// Check if a dependency matches a pattern (supports wildcards)
116    pub fn matches_pattern(dependency: &str, pattern: &str) -> bool {
117        if pattern.contains('*') {
118            // Simple wildcard matching
119            let parts: Vec<&str> = pattern.split('*').collect();
120            if parts.len() == 2 {
121                dependency.starts_with(parts[0]) && dependency.ends_with(parts[1])
122            } else {
123                dependency.contains(&pattern.replace('*', ""))
124            }
125        } else {
126            dependency == pattern || dependency.contains(pattern)
127        }
128    }
129
130    /// Resolves conflicts between mutually exclusive technologies
131    pub fn resolve_technology_conflicts(technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
132        let mut resolved = Vec::new();
133        let mut name_to_tech: HashMap<String, DetectedTechnology> = HashMap::new();
134        
135        // First pass: collect all technologies
136        for tech in technologies {
137            if let Some(existing) = name_to_tech.get(&tech.name) {
138                // Keep the one with higher confidence
139                if tech.confidence > existing.confidence {
140                    name_to_tech.insert(tech.name.clone(), tech);
141                }
142            } else {
143                name_to_tech.insert(tech.name.clone(), tech);
144            }
145        }
146        
147        // Second pass: resolve conflicts
148        let all_techs: Vec<_> = name_to_tech.values().collect();
149        let mut excluded_names = std::collections::HashSet::new();
150        
151        for tech in &all_techs {
152            if excluded_names.contains(&tech.name) {
153                continue;
154            }
155            
156            // Check for conflicts
157            for conflict in &tech.conflicts_with {
158                if let Some(conflicting_tech) = name_to_tech.get(conflict) {
159                    if tech.confidence > conflicting_tech.confidence {
160                        excluded_names.insert(conflict.clone());
161                        log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})", 
162                                  conflict, conflicting_tech.confidence, tech.name, tech.confidence);
163                    } else {
164                        excluded_names.insert(tech.name.clone());
165                        log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})", 
166                                  tech.name, tech.confidence, conflict, conflicting_tech.confidence);
167                        break;
168                    }
169                }
170            }
171        }
172        
173        // Collect non-excluded technologies
174        for tech in name_to_tech.into_values() {
175            if !excluded_names.contains(&tech.name) {
176                resolved.push(tech);
177            }
178        }
179        
180        resolved
181    }
182
183    /// Marks technologies that are primary drivers of the application architecture
184    pub fn mark_primary_technologies(mut technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
185        use crate::analyzer::TechnologyCategory;
186        
187        // Meta-frameworks are always primary
188        let mut has_meta_framework = false;
189        for tech in &mut technologies {
190            if matches!(tech.category, TechnologyCategory::MetaFramework) {
191                tech.is_primary = true;
192                has_meta_framework = true;
193            }
194        }
195        
196        // If no meta-framework, mark the highest confidence backend or frontend framework as primary
197        if !has_meta_framework {
198            let mut best_framework: Option<usize> = None;
199            let mut best_confidence = 0.0;
200            
201            for (i, tech) in technologies.iter().enumerate() {
202                if matches!(tech.category, TechnologyCategory::BackendFramework | TechnologyCategory::FrontendFramework) {
203                    if tech.confidence > best_confidence {
204                        best_confidence = tech.confidence;
205                        best_framework = Some(i);
206                    }
207                }
208            }
209            
210            if let Some(index) = best_framework {
211                technologies[index].is_primary = true;
212            }
213        }
214        
215        technologies
216    }
217}