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}
36
37pub struct FrameworkDetectionUtils;
39
40impl FrameworkDetectionUtils {
41 pub fn detect_technologies_by_dependencies(
43 rules: &[TechnologyRule],
44 dependencies: &[String],
45 base_confidence: f32,
46 ) -> Vec<DetectedTechnology> {
47 let mut technologies = Vec::new();
48
49 let tanstack_deps: Vec<_> = dependencies.iter()
51 .filter(|dep| dep.contains("tanstack") || dep.contains("vinxi"))
52 .collect();
53 if !tanstack_deps.is_empty() {
54 log::debug!("Found potential Tanstack dependencies: {:?}", tanstack_deps);
55 }
56
57 for rule in rules {
58 let mut matches = 0;
59 let total_patterns = rule.dependency_patterns.len();
60
61 if total_patterns == 0 {
62 continue;
63 }
64
65 for pattern in &rule.dependency_patterns {
66 let matching_deps: Vec<_> = dependencies.iter()
67 .filter(|dep| Self::matches_pattern(dep, pattern))
68 .collect();
69
70 if !matching_deps.is_empty() {
71 matches += 1;
72
73 if rule.name.contains("Tanstack") {
75 log::debug!("Tanstack Start: Pattern '{}' matched dependencies: {:?}", pattern, matching_deps);
76 }
77 }
78 }
79
80 if matches > 0 {
82 let pattern_confidence = matches as f32 / total_patterns as f32;
83 let final_confidence = (rule.confidence * pattern_confidence + base_confidence * 0.1).min(1.0);
86
87 if rule.name.contains("Tanstack") {
89 log::debug!("Tanstack Start detected with {} matches out of {} patterns, confidence: {:.2}",
90 matches, total_patterns, final_confidence);
91 }
92
93 technologies.push(DetectedTechnology {
94 name: rule.name.clone(),
95 version: None, category: rule.category.clone(),
97 confidence: final_confidence,
98 requires: rule.requires.clone(),
99 conflicts_with: rule.conflicts_with.clone(),
100 is_primary: rule.is_primary_indicator,
101 });
102 } else if rule.name.contains("Tanstack") {
103 log::debug!("Tanstack Start not detected - no patterns matched. Available dependencies: {:?}",
105 dependencies.iter().take(10).collect::<Vec<_>>());
106 }
107 }
108
109 technologies
110 }
111
112 pub fn matches_pattern(dependency: &str, pattern: &str) -> bool {
114 if pattern.contains('*') {
115 let parts: Vec<&str> = pattern.split('*').collect();
117 if parts.len() == 2 {
118 dependency.starts_with(parts[0]) && dependency.ends_with(parts[1])
119 } else {
120 dependency.contains(&pattern.replace('*', ""))
121 }
122 } else {
123 dependency == pattern || dependency.contains(pattern)
124 }
125 }
126
127 pub fn resolve_technology_conflicts(technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
129 let mut resolved = Vec::new();
130 let mut name_to_tech: HashMap<String, DetectedTechnology> = HashMap::new();
131
132 for tech in technologies {
134 if let Some(existing) = name_to_tech.get(&tech.name) {
135 if tech.confidence > existing.confidence {
137 name_to_tech.insert(tech.name.clone(), tech);
138 }
139 } else {
140 name_to_tech.insert(tech.name.clone(), tech);
141 }
142 }
143
144 let all_techs: Vec<_> = name_to_tech.values().collect();
146 let mut excluded_names = std::collections::HashSet::new();
147
148 for tech in &all_techs {
149 if excluded_names.contains(&tech.name) {
150 continue;
151 }
152
153 for conflict in &tech.conflicts_with {
155 if let Some(conflicting_tech) = name_to_tech.get(conflict) {
156 if tech.confidence > conflicting_tech.confidence {
157 excluded_names.insert(conflict.clone());
158 log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})",
159 conflict, conflicting_tech.confidence, tech.name, tech.confidence);
160 } else {
161 excluded_names.insert(tech.name.clone());
162 log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})",
163 tech.name, tech.confidence, conflict, conflicting_tech.confidence);
164 break;
165 }
166 }
167 }
168 }
169
170 for tech in name_to_tech.into_values() {
172 if !excluded_names.contains(&tech.name) {
173 resolved.push(tech);
174 }
175 }
176
177 resolved
178 }
179
180 pub fn mark_primary_technologies(mut technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
182 use crate::analyzer::TechnologyCategory;
183
184 let mut has_meta_framework = false;
186 for tech in &mut technologies {
187 if matches!(tech.category, TechnologyCategory::MetaFramework) {
188 tech.is_primary = true;
189 has_meta_framework = true;
190 }
191 }
192
193 if !has_meta_framework {
195 let mut best_framework: Option<usize> = None;
196 let mut best_confidence = 0.0;
197
198 for (i, tech) in technologies.iter().enumerate() {
199 if matches!(tech.category, TechnologyCategory::BackendFramework | TechnologyCategory::FrontendFramework) {
200 if tech.confidence > best_confidence {
201 best_confidence = tech.confidence;
202 best_framework = Some(i);
203 }
204 }
205 }
206
207 if let Some(index) = best_framework {
208 technologies[index].is_primary = true;
209 }
210 }
211
212 technologies
213 }
214}