syncable_cli/analyzer/frameworks/
mod.rs

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