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                // Java/Maven style: spring-boot matches spring-boot-starter-web
146                || dependency.starts_with(&(pattern.to_string() + "-"))
147                // Maven groupId:artifactId style: org.springframework matches org.springframework.boot:spring-boot
148                || dependency.starts_with(&(pattern.to_string() + "."))
149                || dependency.starts_with(&(pattern.to_string() + ":"))
150                // Maven artifactId contains the pattern (e.g., "spring" in "spring-boot-starter-web")
151                || dependency.contains(&format!("-{}-", pattern))
152                || dependency.contains(&format!(":{}", pattern))
153        }
154    }
155
156    /// Resolves conflicts between mutually exclusive technologies
157    pub fn resolve_technology_conflicts(
158        technologies: Vec<DetectedTechnology>,
159    ) -> Vec<DetectedTechnology> {
160        let mut resolved = Vec::new();
161        let mut name_to_tech: HashMap<String, DetectedTechnology> = HashMap::new();
162
163        // First pass: collect all technologies
164        for tech in technologies {
165            if let Some(existing) = name_to_tech.get(&tech.name) {
166                // Keep the one with higher confidence
167                if tech.confidence > existing.confidence {
168                    name_to_tech.insert(tech.name.clone(), tech);
169                }
170            } else {
171                name_to_tech.insert(tech.name.clone(), tech);
172            }
173        }
174
175        // Second pass: resolve conflicts
176        let all_techs: Vec<_> = name_to_tech.values().collect();
177        let mut excluded_names = std::collections::HashSet::new();
178
179        for tech in &all_techs {
180            if excluded_names.contains(&tech.name) {
181                continue;
182            }
183
184            // Check for conflicts
185            for conflict in &tech.conflicts_with {
186                if let Some(conflicting_tech) = name_to_tech.get(conflict) {
187                    if tech.confidence > conflicting_tech.confidence {
188                        excluded_names.insert(conflict.clone());
189                        log::info!(
190                            "Excluding {} (confidence: {}) in favor of {} (confidence: {})",
191                            conflict,
192                            conflicting_tech.confidence,
193                            tech.name,
194                            tech.confidence
195                        );
196                    } else {
197                        excluded_names.insert(tech.name.clone());
198                        log::info!(
199                            "Excluding {} (confidence: {}) in favor of {} (confidence: {})",
200                            tech.name,
201                            tech.confidence,
202                            conflict,
203                            conflicting_tech.confidence
204                        );
205                        break;
206                    }
207                }
208            }
209        }
210
211        // Collect non-excluded technologies
212        for tech in name_to_tech.into_values() {
213            if !excluded_names.contains(&tech.name) {
214                resolved.push(tech);
215            }
216        }
217
218        resolved
219    }
220
221    /// Marks technologies that are primary drivers of the application architecture
222    pub fn mark_primary_technologies(
223        mut technologies: Vec<DetectedTechnology>,
224    ) -> Vec<DetectedTechnology> {
225        use crate::analyzer::TechnologyCategory;
226
227        // Meta-frameworks are always primary
228        let mut has_meta_framework = false;
229        for tech in &mut technologies {
230            if matches!(tech.category, TechnologyCategory::MetaFramework) {
231                tech.is_primary = true;
232                has_meta_framework = true;
233            }
234        }
235
236        // If no meta-framework, mark the highest confidence backend or frontend framework as primary
237        if !has_meta_framework {
238            let mut best_framework: Option<usize> = None;
239            let mut best_confidence = 0.0;
240
241            for (i, tech) in technologies.iter().enumerate() {
242                if matches!(
243                    tech.category,
244                    TechnologyCategory::BackendFramework | TechnologyCategory::FrontendFramework
245                ) && tech.confidence > best_confidence
246                {
247                    best_confidence = tech.confidence;
248                    best_framework = Some(i);
249                }
250            }
251
252            if let Some(index) = best_framework {
253                technologies[index].is_primary = true;
254            }
255        }
256
257        technologies
258    }
259}