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(1.0);
88
89 if rule.name.contains("Tanstack") {
91 log::debug!("Tanstack Start detected with {} matches out of {} patterns, confidence: {:.2}",
92 matches, total_patterns, final_confidence);
93 }
94
95 technologies.push(DetectedTechnology {
96 name: rule.name.clone(),
97 version: None, category: rule.category.clone(),
99 confidence: final_confidence,
100 requires: rule.requires.clone(),
101 conflicts_with: rule.conflicts_with.clone(),
102 is_primary: rule.is_primary_indicator,
103 file_indicators: rule.file_indicators.clone(),
104 });
105 } else if rule.name.contains("Tanstack") {
106 log::debug!("Tanstack Start not detected - no patterns matched. Available dependencies: {:?}",
108 dependencies.iter().take(10).collect::<Vec<_>>());
109 }
110 }
111
112 technologies
113 }
114
115 pub fn matches_pattern(dependency: &str, pattern: &str) -> bool {
117 if pattern.contains('*') {
118 let parts: Vec<&str> = pattern.split('*').collect();
120 if parts.len() == 2 {
121 dependency.starts_with(parts[0]) && dependency.ends_with(parts[1])
122 } else {
123 dependency.contains(&pattern.replace('*', ""))
124 }
125 } else {
126 dependency == pattern || dependency.contains(pattern)
127 }
128 }
129
130 pub fn resolve_technology_conflicts(technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
132 let mut resolved = Vec::new();
133 let mut name_to_tech: HashMap<String, DetectedTechnology> = HashMap::new();
134
135 for tech in technologies {
137 if let Some(existing) = name_to_tech.get(&tech.name) {
138 if tech.confidence > existing.confidence {
140 name_to_tech.insert(tech.name.clone(), tech);
141 }
142 } else {
143 name_to_tech.insert(tech.name.clone(), tech);
144 }
145 }
146
147 let all_techs: Vec<_> = name_to_tech.values().collect();
149 let mut excluded_names = std::collections::HashSet::new();
150
151 for tech in &all_techs {
152 if excluded_names.contains(&tech.name) {
153 continue;
154 }
155
156 for conflict in &tech.conflicts_with {
158 if let Some(conflicting_tech) = name_to_tech.get(conflict) {
159 if tech.confidence > conflicting_tech.confidence {
160 excluded_names.insert(conflict.clone());
161 log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})",
162 conflict, conflicting_tech.confidence, tech.name, tech.confidence);
163 } else {
164 excluded_names.insert(tech.name.clone());
165 log::info!("Excluding {} (confidence: {}) in favor of {} (confidence: {})",
166 tech.name, tech.confidence, conflict, conflicting_tech.confidence);
167 break;
168 }
169 }
170 }
171 }
172
173 for tech in name_to_tech.into_values() {
175 if !excluded_names.contains(&tech.name) {
176 resolved.push(tech);
177 }
178 }
179
180 resolved
181 }
182
183 pub fn mark_primary_technologies(mut technologies: Vec<DetectedTechnology>) -> Vec<DetectedTechnology> {
185 use crate::analyzer::TechnologyCategory;
186
187 let mut has_meta_framework = false;
189 for tech in &mut technologies {
190 if matches!(tech.category, TechnologyCategory::MetaFramework) {
191 tech.is_primary = true;
192 has_meta_framework = true;
193 }
194 }
195
196 if !has_meta_framework {
198 let mut best_framework: Option<usize> = None;
199 let mut best_confidence = 0.0;
200
201 for (i, tech) in technologies.iter().enumerate() {
202 if matches!(tech.category, TechnologyCategory::BackendFramework | TechnologyCategory::FrontendFramework) {
203 if tech.confidence > best_confidence {
204 best_confidence = tech.confidence;
205 best_framework = Some(i);
206 }
207 }
208 }
209
210 if let Some(index) = best_framework {
211 technologies[index].is_primary = true;
212 }
213 }
214
215 technologies
216 }
217}