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                // Cap dependency-based confidence at 0.95 to ensure file-based detection (1.0) takes precedence
88                let final_confidence = (rule.confidence * pattern_confidence + base_confidence * 0.1).min(0.95);
89                
90                // Debug logging for Tanstack Start detection
91                if rule.name.contains("Tanstack") {
92                    log::debug!("Tanstack Start detected with {} matches out of {} patterns, confidence: {:.2}", 
93                              matches, total_patterns, final_confidence);
94                }
95                
96                technologies.push(DetectedTechnology {
97                    name: rule.name.clone(),
98                    version: None, // TODO: Extract version from dependencies
99                    category: rule.category.clone(),
100                    confidence: final_confidence,
101                    requires: rule.requires.clone(),
102                    conflicts_with: rule.conflicts_with.clone(),
103                    is_primary: rule.is_primary_indicator,
104                    file_indicators: rule.file_indicators.clone(),
105                });
106            } else if rule.name.contains("Tanstack") {
107                // Debug logging when Tanstack Start is not detected
108                log::debug!("Tanstack Start not detected - no patterns matched. Available dependencies: {:?}", 
109                          dependencies.iter().take(10).collect::<Vec<_>>());
110            }
111        }
112        
113        technologies
114    }
115
116    /// Check if a dependency matches a pattern (supports wildcards)
117    pub fn matches_pattern(dependency: &str, pattern: &str) -> bool {
118        if pattern.contains('*') {
119            // Simple wildcard matching
120            let parts: Vec<&str> = pattern.split('*').collect();
121            if parts.len() == 2 {
122                dependency.starts_with(parts[0]) && dependency.ends_with(parts[1])
123            } else {
124                dependency.contains(&pattern.replace('*', ""))
125            }
126        } else {
127            // For dependency detection, use exact matching to avoid false positives
128            // Only match if the dependency is exactly the pattern or starts with the pattern followed by a version specifier
129            dependency == pattern || dependency.starts_with(&(pattern.to_string() + "@")) || dependency.starts_with(&(pattern.to_string() + "/"))
130        }
131    }
132
133    /// Resolves conflicts between mutually exclusive technologies
134    pub fn resolve_technology_conflicts(technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
135        let mut resolved = Vec::new();
136        let mut name_to_tech: HashMap<String, DetectedTechnology> = HashMap::new();
137        
138        // First pass: collect all technologies
139        for tech in technologies {
140            if let Some(existing) = name_to_tech.get(&tech.name) {
141                // Keep the one with higher confidence
142                if tech.confidence > existing.confidence {
143                    name_to_tech.insert(tech.name.clone(), tech);
144                }
145            } else {
146                name_to_tech.insert(tech.name.clone(), tech);
147            }
148        }
149        
150        // Second pass: resolve conflicts
151        let all_techs: Vec<_> = name_to_tech.values().collect();
152        let mut excluded_names = std::collections::HashSet::new();
153        
154        for tech in &all_techs {
155            if excluded_names.contains(&tech.name) {
156                continue;
157            }
158            
159            // Check for conflicts
160            for conflict in &tech.conflicts_with {
161                if let Some(conflicting_tech) = name_to_tech.get(conflict) {
162                    if tech.confidence > conflicting_tech.confidence {
163                        excluded_names.insert(conflict.clone());
164                        log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})", 
165                                  conflict, conflicting_tech.confidence, tech.name, tech.confidence);
166                    } else {
167                        excluded_names.insert(tech.name.clone());
168                        log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})", 
169                                  tech.name, tech.confidence, conflict, conflicting_tech.confidence);
170                        break;
171                    }
172                }
173            }
174        }
175        
176        // Collect non-excluded technologies
177        for tech in name_to_tech.into_values() {
178            if !excluded_names.contains(&tech.name) {
179                resolved.push(tech);
180            }
181        }
182        
183        resolved
184    }
185
186    /// Marks technologies that are primary drivers of the application architecture
187    pub fn mark_primary_technologies(mut technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
188        use crate::analyzer::TechnologyCategory;
189        
190        // Meta-frameworks are always primary
191        let mut has_meta_framework = false;
192        for tech in &mut technologies {
193            if matches!(tech.category, TechnologyCategory::MetaFramework) {
194                tech.is_primary = true;
195                has_meta_framework = true;
196            }
197        }
198        
199        // If no meta-framework, mark the highest confidence backend or frontend framework as primary
200        if !has_meta_framework {
201            let mut best_framework: Option<usize> = None;
202            let mut best_confidence = 0.0;
203            
204            for (i, tech) in technologies.iter().enumerate() {
205                if matches!(tech.category, TechnologyCategory::BackendFramework | TechnologyCategory::FrontendFramework) {
206                    if tech.confidence > best_confidence {
207                        best_confidence = tech.confidence;
208                        best_framework = Some(i);
209                    }
210                }
211            }
212            
213            if let Some(index) = best_framework {
214                technologies[index].is_primary = true;
215            }
216        }
217        
218        technologies
219    }
220}