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