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 || dependency.starts_with(&(pattern.to_string() + "-"))
147 || dependency.starts_with(&(pattern.to_string() + "."))
149 || dependency.starts_with(&(pattern.to_string() + ":"))
150 || dependency.contains(&format!("-{}-", pattern))
152 || dependency.contains(&format!(":{}", pattern))
153 }
154 }
155
156 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 for tech in technologies {
165 if let Some(existing) = name_to_tech.get(&tech.name) {
166 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 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 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 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 pub fn mark_primary_technologies(
223 mut technologies: Vec<DetectedTechnology>,
224 ) -> Vec<DetectedTechnology> {
225 use crate::analyzer::TechnologyCategory;
226
227 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 !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}