syncable_cli/analyzer/frameworks/
mod.rs1pub 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
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
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 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 if matches > 0 {
90 let pattern_confidence = matches as f32 / total_patterns as f32;
91 let final_confidence =
95 (rule.confidence * pattern_confidence + base_confidence * 0.1).min(0.95);
96
97 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, 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 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 pub fn matches_pattern(dependency: &str, pattern: &str) -> bool {
131 if pattern.contains('*') {
132 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 dependency == pattern
143 || dependency.starts_with(&(pattern.to_string() + "@"))
144 || dependency.starts_with(&(pattern.to_string() + "/"))
145 }
146 }
147
148 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 for tech in technologies {
157 if let Some(existing) = name_to_tech.get(&tech.name) {
158 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 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 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 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 pub fn mark_primary_technologies(
215 mut technologies: Vec<DetectedTechnology>,
216 ) -> Vec<DetectedTechnology> {
217 use crate::analyzer::TechnologyCategory;
218
219 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 !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}