syncable_cli/analyzer/frameworks/
mod.rs1pub 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
11pub trait LanguageFrameworkDetector {
13 fn detect_frameworks(&self, language: &DetectedLanguage) -> Result<Vec<DetectedTechnology>>;
15
16 fn supported_languages(&self) -> Vec<&'static str>;
18}
19
20#[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 pub requires: Vec<String>,
29 pub conflicts_with: Vec<String>,
31 pub is_primary_indicator: bool,
33 pub alternative_names: Vec<String>,
35 pub file_indicators: Vec<String>,
37}
38
39pub struct FrameworkDetectionUtils;
41
42impl FrameworkDetectionUtils {
43 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 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 if rule.name.contains("Tanstack") {
77 log::debug!("Tanstack Start: Pattern '{}' matched dependencies: {:?}", pattern, matching_deps);
78 }
79 }
80 }
81
82 if matches > 0 {
84 let pattern_confidence = matches as f32 / total_patterns as f32;
85 let final_confidence = (rule.confidence * pattern_confidence + base_confidence * 0.1).min(0.95);
89
90 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, 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 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 pub fn matches_pattern(dependency: &str, pattern: &str) -> bool {
118 if pattern.contains('*') {
119 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 dependency == pattern || dependency.starts_with(&(pattern.to_string() + "@")) || dependency.starts_with(&(pattern.to_string() + "/"))
130 }
131 }
132
133 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 for tech in technologies {
140 if let Some(existing) = name_to_tech.get(&tech.name) {
141 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 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 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 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 pub fn mark_primary_technologies(mut technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
188 use crate::analyzer::TechnologyCategory;
189
190 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 !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}