raz_core/
framework_detection.rs

1//! Framework detection with precise pattern matching and scoring
2//!
3//! This module provides laser-precise framework detection by analyzing:
4//! - Dependencies and their combinations
5//! - Project structure patterns
6//! - File names and locations
7//! - Function contexts
8//! - Configuration files
9
10use crate::ProjectContext;
11use std::collections::HashMap;
12use std::path::Path;
13
14/// Framework types that can be detected
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum FrameworkType {
17    LeptosSSR,
18    LeptosCSR,
19    DioxusWeb,
20    DioxusDesktop,
21    DioxusMobile,
22    BevyGame,
23    TauriDesktop,
24    ActixWeb,
25    Axum,
26    Rocket,
27    YewWeb,
28    EguiDesktop,
29    WasmPack,
30    VanillaRust,
31}
32
33/// Detection confidence score
34#[derive(Debug, Clone)]
35pub struct DetectionScore {
36    pub framework: FrameworkType,
37    pub confidence: f32,
38    pub reasons: Vec<String>,
39}
40
41/// Framework detection engine with precise pattern matching
42pub struct PreciseFrameworkDetector {
43    patterns: HashMap<FrameworkType, FrameworkPattern>,
44}
45
46/// Pattern definition for framework detection
47#[derive(Debug, Clone)]
48struct FrameworkPattern {
49    /// Required dependencies (all must be present)
50    required_deps: Vec<&'static str>,
51    /// Optional dependencies that increase confidence
52    optional_deps: Vec<&'static str>,
53    /// File structure patterns
54    file_patterns: Vec<&'static str>,
55    /// Configuration file patterns
56    config_files: Vec<&'static str>,
57    /// Typical entry point patterns
58    entry_points: Vec<&'static str>,
59    /// Weight for this framework (higher = more specific)
60    specificity: f32,
61}
62
63impl Default for PreciseFrameworkDetector {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl PreciseFrameworkDetector {
70    pub fn new() -> Self {
71        let mut patterns = HashMap::new();
72
73        // Leptos SSR pattern (most specific)
74        patterns.insert(
75            FrameworkType::LeptosSSR,
76            FrameworkPattern {
77                required_deps: vec!["leptos", "leptos_axum"],
78                optional_deps: vec![
79                    "leptos_meta",
80                    "leptos_router",
81                    "axum",
82                    "tokio",
83                    "tower",
84                    "tower-http",
85                ],
86                file_patterns: vec![
87                    "app/src/lib.rs",
88                    "frontend/src/lib.rs",
89                    "server/src/main.rs",
90                    "style/main.scss",
91                    "Cargo.toml",
92                ],
93                config_files: vec!["Cargo.toml", "Trunk.toml", "style.css"],
94                entry_points: vec!["server/src/main.rs", "src/main.rs"],
95                specificity: 10.0,
96            },
97        );
98
99        // Leptos CSR pattern
100        patterns.insert(
101            FrameworkType::LeptosCSR,
102            FrameworkPattern {
103                required_deps: vec!["leptos"],
104                optional_deps: vec!["leptos_meta", "leptos_router", "wasm-bindgen", "web-sys"],
105                file_patterns: vec!["src/main.rs", "src/app.rs", "index.html", "Trunk.toml"],
106                config_files: vec!["Trunk.toml", "index.html"],
107                entry_points: vec!["src/main.rs"],
108                specificity: 8.0,
109            },
110        );
111
112        // Dioxus Web pattern - matches both separate crates and features
113        patterns.insert(
114            FrameworkType::DioxusWeb,
115            FrameworkPattern {
116                required_deps: vec!["dioxus"],
117                optional_deps: vec![
118                    "dioxus-web",
119                    "dioxus-router",
120                    "dioxus-hooks",
121                    "wasm-bindgen",
122                ],
123                file_patterns: vec!["src/main.rs", "index.html", "Dioxus.toml"],
124                config_files: vec!["Dioxus.toml"],
125                entry_points: vec!["src/main.rs"],
126                specificity: 8.0,
127            },
128        );
129
130        // Dioxus Desktop pattern - matches both separate crates and features
131        patterns.insert(
132            FrameworkType::DioxusDesktop,
133            FrameworkPattern {
134                required_deps: vec!["dioxus"],
135                optional_deps: vec!["dioxus-desktop", "dioxus-router", "dioxus-hooks"],
136                file_patterns: vec!["src/main.rs", "Dioxus.toml"],
137                config_files: vec!["Dioxus.toml"],
138                entry_points: vec!["src/main.rs"],
139                specificity: 8.0,
140            },
141        );
142
143        // Dioxus Mobile pattern - matches both separate crates and features
144        patterns.insert(
145            FrameworkType::DioxusMobile,
146            FrameworkPattern {
147                required_deps: vec!["dioxus"],
148                optional_deps: vec!["dioxus-mobile", "dioxus-router", "dioxus-hooks"],
149                file_patterns: vec!["src/main.rs", "Dioxus.toml"],
150                config_files: vec!["Dioxus.toml"],
151                entry_points: vec!["src/main.rs"],
152                specificity: 8.0,
153            },
154        );
155
156        // Bevy game pattern
157        patterns.insert(
158            FrameworkType::BevyGame,
159            FrameworkPattern {
160                required_deps: vec!["bevy"],
161                optional_deps: vec!["bevy_egui", "bevy_rapier3d", "bevy_rapier2d"],
162                file_patterns: vec!["src/main.rs", "assets/"],
163                config_files: vec![],
164                entry_points: vec!["src/main.rs"],
165                specificity: 7.0,
166            },
167        );
168
169        // Tauri desktop pattern
170        patterns.insert(
171            FrameworkType::TauriDesktop,
172            FrameworkPattern {
173                required_deps: vec!["tauri"],
174                optional_deps: vec!["serde", "serde_json", "tokio", "tauri-build"],
175                file_patterns: vec![
176                    "src-tauri/src/main.rs",
177                    "src-tauri/tauri.conf.json",
178                    "src/main.rs",     // When running from within src-tauri
179                    "tauri.conf.json", // When running from within src-tauri
180                    "src/main.js",
181                    "index.html",
182                ],
183                config_files: vec!["src-tauri/tauri.conf.json", "tauri.conf.json"],
184                entry_points: vec!["src-tauri/src/main.rs", "src/main.rs"],
185                specificity: 9.5, // High specificity for Tauri
186            },
187        );
188
189        // Actix Web pattern
190        patterns.insert(
191            FrameworkType::ActixWeb,
192            FrameworkPattern {
193                required_deps: vec!["actix-web"],
194                optional_deps: vec!["actix-rt", "actix-cors", "actix-session"],
195                file_patterns: vec!["src/main.rs"],
196                config_files: vec![],
197                entry_points: vec!["src/main.rs"],
198                specificity: 5.0,
199            },
200        );
201
202        // Axum pattern (without Leptos)
203        patterns.insert(
204            FrameworkType::Axum,
205            FrameworkPattern {
206                required_deps: vec!["axum"],
207                optional_deps: vec!["tower", "tower-http", "tokio"],
208                file_patterns: vec!["src/main.rs"],
209                config_files: vec![],
210                entry_points: vec!["src/main.rs"],
211                specificity: 4.0, // Lower than Leptos SSR which also uses Axum
212            },
213        );
214
215        // Rocket pattern
216        patterns.insert(
217            FrameworkType::Rocket,
218            FrameworkPattern {
219                required_deps: vec!["rocket"],
220                optional_deps: vec!["rocket_contrib", "serde", "serde_json"],
221                file_patterns: vec!["src/main.rs", "Rocket.toml"],
222                config_files: vec!["Rocket.toml"],
223                entry_points: vec!["src/main.rs"],
224                specificity: 6.0,
225            },
226        );
227
228        // Yew pattern
229        patterns.insert(
230            FrameworkType::YewWeb,
231            FrameworkPattern {
232                required_deps: vec!["yew"],
233                optional_deps: vec!["yew-router", "wasm-bindgen", "web-sys", "js-sys"],
234                file_patterns: vec![
235                    "src/main.rs",
236                    "index.html",
237                    "Trunk.toml",
238                    "index.scss",
239                    "style.css",
240                ],
241                config_files: vec!["Trunk.toml", "index.html"],
242                entry_points: vec!["src/main.rs"],
243                specificity: 9.0, // High specificity for Yew projects
244            },
245        );
246
247        // Egui Desktop pattern
248        patterns.insert(
249            FrameworkType::EguiDesktop,
250            FrameworkPattern {
251                required_deps: vec!["egui", "eframe"],
252                optional_deps: vec!["egui_extras"],
253                file_patterns: vec!["src/main.rs"],
254                config_files: vec![],
255                entry_points: vec!["src/main.rs"],
256                specificity: 6.0,
257            },
258        );
259
260        // WASM Pack pattern
261        patterns.insert(
262            FrameworkType::WasmPack,
263            FrameworkPattern {
264                required_deps: vec!["wasm-bindgen"],
265                optional_deps: vec!["web-sys", "js-sys", "wasm-bindgen-futures"],
266                file_patterns: vec!["src/lib.rs", "Cargo.toml", "pkg/"],
267                config_files: vec![],
268                entry_points: vec!["src/lib.rs"],
269                specificity: 5.0,
270            },
271        );
272
273        Self { patterns }
274    }
275
276    /// Detect framework with scoring algorithm
277    pub fn detect(
278        &self,
279        context: &ProjectContext,
280        file_path: Option<&Path>,
281    ) -> Vec<DetectionScore> {
282        let mut scores = Vec::new();
283
284        for (framework, pattern) in &self.patterns {
285            let mut score = 0.0;
286            let mut reasons = Vec::new();
287
288            // Check required dependencies (must have all)
289            let has_all_required = pattern
290                .required_deps
291                .iter()
292                .all(|dep| context.dependencies.iter().any(|d| d.name == *dep));
293
294            if !has_all_required {
295                continue; // Skip this framework if missing required deps
296            }
297
298            score += pattern.specificity;
299            reasons.push(format!(
300                "Has all required dependencies: {:?}",
301                pattern.required_deps
302            ));
303
304            // Check optional dependencies (bonus points)
305            let optional_count = pattern
306                .optional_deps
307                .iter()
308                .filter(|dep| context.dependencies.iter().any(|d| d.name == **dep))
309                .count();
310
311            if optional_count > 0 {
312                score += optional_count as f32 * 0.5;
313                reasons.push(format!("Has {optional_count} optional dependencies"));
314            }
315
316            // Check file structure patterns
317            let file_pattern_matches = pattern
318                .file_patterns
319                .iter()
320                .filter(|pattern| {
321                    let path = context.workspace_root.join(pattern);
322                    path.exists()
323                })
324                .count();
325
326            if file_pattern_matches > 0 {
327                score += file_pattern_matches as f32 * 1.0;
328                reasons.push(format!(
329                    "Matches {file_pattern_matches} file structure patterns"
330                ));
331            }
332
333            // Check config files
334            let config_matches = pattern
335                .config_files
336                .iter()
337                .filter(|config| {
338                    let path = context.workspace_root.join(config);
339                    path.exists()
340                })
341                .count();
342
343            if config_matches > 0 {
344                score += config_matches as f32 * 2.0;
345                reasons.push(format!("Has {config_matches} config files"));
346            }
347
348            // Check if current file matches entry point
349            if let Some(current_file) = file_path {
350                for entry_point in &pattern.entry_points {
351                    let entry_path = context.workspace_root.join(entry_point);
352                    if current_file == entry_path {
353                        score += 3.0;
354                        reasons.push(format!("Current file is entry point: {entry_point}"));
355                        break;
356                    }
357                }
358            }
359
360            // Special case: Leptos SSR structure detection
361            if framework == &FrameworkType::LeptosSSR {
362                // Check for typical Leptos SSR workspace structure
363                let has_app_crate = context.workspace_members.iter().any(|m| m.name == "app");
364                let has_frontend_crate = context
365                    .workspace_members
366                    .iter()
367                    .any(|m| m.name == "frontend");
368                let has_server_crate = context.workspace_members.iter().any(|m| m.name == "server");
369
370                if has_app_crate || has_frontend_crate {
371                    score += 2.0;
372                    reasons
373                        .push("Has Leptos workspace structure (app/frontend crates)".to_string());
374                }
375
376                if has_server_crate {
377                    score += 2.0;
378                    reasons.push("Has server crate typical of Leptos SSR".to_string());
379                }
380            }
381
382            // Special case: Dioxus platform detection
383            if framework == &FrameworkType::DioxusDesktop
384                || framework == &FrameworkType::DioxusWeb
385                || framework == &FrameworkType::DioxusMobile
386            {
387                score += self.detect_dioxus_platform_specifics(context, framework, &mut reasons);
388            }
389
390            scores.push(DetectionScore {
391                framework: framework.clone(),
392                confidence: score,
393                reasons,
394            });
395        }
396
397        // Sort by confidence (highest first)
398        scores.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
399
400        scores
401    }
402
403    /// Detect Dioxus platform-specific features and configuration
404    fn detect_dioxus_platform_specifics(
405        &self,
406        context: &ProjectContext,
407        framework: &FrameworkType,
408        reasons: &mut Vec<String>,
409    ) -> f32 {
410        let mut bonus_score = 0.0;
411
412        // Check workspace structure for platform-specific folders
413        for member in &context.workspace_members {
414            let member_name = member.name.to_lowercase();
415            let path_name = member
416                .path
417                .file_name()
418                .and_then(|name| name.to_str())
419                .unwrap_or("")
420                .to_lowercase();
421
422            match framework {
423                FrameworkType::DioxusWeb => {
424                    if member_name.contains("web") || path_name.contains("web") {
425                        bonus_score += 5.0;
426                        reasons.push(format!("Found web workspace member: {}", member.name));
427                    }
428                }
429                FrameworkType::DioxusDesktop => {
430                    if member_name.contains("desktop") || path_name.contains("desktop") {
431                        bonus_score += 5.0;
432                        reasons.push(format!("Found desktop workspace member: {}", member.name));
433                    }
434                }
435                FrameworkType::DioxusMobile => {
436                    if member_name.contains("mobile") || path_name.contains("mobile") {
437                        bonus_score += 5.0;
438                        reasons.push(format!("Found mobile workspace member: {}", member.name));
439                    }
440                }
441                _ => {}
442            }
443        }
444
445        // Check for Dioxus.toml configuration
446        let dioxus_toml_path = context.workspace_root.join("Dioxus.toml");
447        if dioxus_toml_path.exists() {
448            bonus_score += 2.0;
449            reasons.push("Has Dioxus.toml configuration file".to_string());
450
451            // Parse Dioxus.toml to check default_platform
452            if let Ok(content) = std::fs::read_to_string(&dioxus_toml_path) {
453                if let Ok(config) = content.parse::<toml::Value>() {
454                    if let Some(app_config) = config.get("application") {
455                        if let Some(default_platform) = app_config.get("default_platform") {
456                            if let Some(platform_str) = default_platform.as_str() {
457                                match (framework, platform_str) {
458                                    (FrameworkType::DioxusDesktop, "desktop") => {
459                                        bonus_score += 3.0;
460                                        reasons.push(
461                                            "Dioxus.toml specifies desktop platform".to_string(),
462                                        );
463                                    }
464                                    (FrameworkType::DioxusWeb, "web") => {
465                                        bonus_score += 3.0;
466                                        reasons
467                                            .push("Dioxus.toml specifies web platform".to_string());
468                                    }
469                                    (FrameworkType::DioxusMobile, "mobile") => {
470                                        bonus_score += 3.0;
471                                        reasons.push(
472                                            "Dioxus.toml specifies mobile platform".to_string(),
473                                        );
474                                    }
475                                    _ => {}
476                                }
477                            }
478                        }
479                    }
480                }
481            }
482        }
483
484        // Check for Dioxus features in dependencies
485        if let Some(dioxus_dep) = context.dependencies.iter().find(|d| d.name == "dioxus") {
486            match framework {
487                FrameworkType::DioxusDesktop => {
488                    if dioxus_dep.features.contains(&"desktop".to_string()) {
489                        bonus_score += 3.0;
490                        reasons.push("Dioxus dependency has 'desktop' feature".to_string());
491                    }
492                }
493                FrameworkType::DioxusWeb => {
494                    if dioxus_dep.features.contains(&"web".to_string()) {
495                        bonus_score += 3.0;
496                        reasons.push("Dioxus dependency has 'web' feature".to_string());
497                    }
498                }
499                FrameworkType::DioxusMobile => {
500                    if dioxus_dep.features.contains(&"mobile".to_string()) {
501                        bonus_score += 3.0;
502                        reasons.push("Dioxus dependency has 'mobile' feature".to_string());
503                    }
504                }
505                _ => {}
506            }
507
508            // Check for fullstack feature (indicates complex setup)
509            if dioxus_dep.features.contains(&"fullstack".to_string()) {
510                bonus_score += 1.0;
511                reasons.push("Dioxus dependency has 'fullstack' feature".to_string());
512            }
513        }
514
515        // Check default features in Cargo.toml
516        // Parse Cargo.toml to check if mobile is in default features
517        let cargo_toml_path = context.workspace_root.join("Cargo.toml");
518        if cargo_toml_path.exists() {
519            if let Ok(content) = std::fs::read_to_string(&cargo_toml_path) {
520                if let Ok(config) = content.parse::<toml::Value>() {
521                    if let Some(features) = config.get("features") {
522                        if let Some(default_features) = features.get("default") {
523                            if let Some(default_array) = default_features.as_array() {
524                                let default_feature_names: Vec<String> = default_array
525                                    .iter()
526                                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
527                                    .collect();
528
529                                match framework {
530                                    FrameworkType::DioxusMobile => {
531                                        if default_feature_names.contains(&"mobile".to_string()) {
532                                            bonus_score += 4.0;
533                                            reasons
534                                                .push("Mobile is in default features".to_string());
535                                        }
536                                    }
537                                    FrameworkType::DioxusDesktop => {
538                                        if default_feature_names.contains(&"desktop".to_string()) {
539                                            bonus_score += 4.0;
540                                            reasons
541                                                .push("Desktop is in default features".to_string());
542                                        }
543                                        // Desktop is common default, give small bonus
544                                        if default_feature_names.is_empty() {
545                                            bonus_score += 1.0;
546                                        }
547                                    }
548                                    FrameworkType::DioxusWeb => {
549                                        if default_feature_names.contains(&"web".to_string()) {
550                                            bonus_score += 4.0;
551                                            reasons.push("Web is in default features".to_string());
552                                        }
553                                    }
554                                    _ => {}
555                                }
556                            }
557                        }
558                    }
559                }
560            }
561        }
562
563        bonus_score
564    }
565
566    /// Get the best match with minimum confidence threshold
567    pub fn best_match(
568        &self,
569        context: &ProjectContext,
570        file_path: Option<&Path>,
571        min_confidence: f32,
572    ) -> Option<FrameworkType> {
573        let scores = self.detect(context, file_path);
574
575        scores
576            .first()
577            .filter(|s| s.confidence >= min_confidence)
578            .map(|s| s.framework.clone())
579    }
580}
581
582impl FrameworkType {
583    /// Check if this is a web framework
584    pub fn is_web_framework(&self) -> bool {
585        matches!(
586            self,
587            FrameworkType::LeptosSSR
588                | FrameworkType::LeptosCSR
589                | FrameworkType::DioxusWeb
590                | FrameworkType::YewWeb
591                | FrameworkType::ActixWeb
592                | FrameworkType::Axum
593                | FrameworkType::Rocket
594        )
595    }
596
597    /// Get the primary run command for this framework
598    pub fn primary_run_command(&self) -> (&'static str, Vec<&'static str>) {
599        match self {
600            FrameworkType::LeptosSSR => ("cargo", vec!["leptos", "watch"]),
601            FrameworkType::LeptosCSR => ("trunk", vec!["serve"]),
602            FrameworkType::DioxusWeb => ("dx", vec!["serve"]),
603            FrameworkType::DioxusDesktop => ("dx", vec!["serve", "--platform", "desktop"]),
604            FrameworkType::DioxusMobile => ("dx", vec!["serve", "--platform", "mobile"]),
605            FrameworkType::BevyGame => ("cargo", vec!["run"]),
606            FrameworkType::TauriDesktop => ("cargo", vec!["tauri", "dev"]),
607            FrameworkType::ActixWeb => ("cargo", vec!["run"]),
608            FrameworkType::Axum => ("cargo", vec!["run"]),
609            FrameworkType::Rocket => ("cargo", vec!["run"]),
610            FrameworkType::YewWeb => ("trunk", vec!["serve"]),
611            FrameworkType::EguiDesktop => ("cargo", vec!["run"]),
612            FrameworkType::WasmPack => ("wasm-pack", vec!["build"]),
613            FrameworkType::VanillaRust => ("cargo", vec!["run"]),
614        }
615    }
616
617    /// Get the build command for this framework
618    pub fn build_command(&self) -> (&'static str, Vec<&'static str>) {
619        match self {
620            FrameworkType::LeptosSSR => ("cargo", vec!["leptos", "build", "--release"]),
621            FrameworkType::LeptosCSR => ("trunk", vec!["build", "--release"]),
622            FrameworkType::DioxusWeb => ("dx", vec!["build", "--release"]),
623            FrameworkType::DioxusDesktop => ("dx", vec!["build", "--release"]),
624            FrameworkType::DioxusMobile => {
625                ("dx", vec!["build", "--release", "--platform", "mobile"])
626            }
627            FrameworkType::BevyGame => ("cargo", vec!["build", "--release"]),
628            FrameworkType::TauriDesktop => ("cargo", vec!["tauri", "build"]),
629            FrameworkType::ActixWeb => ("cargo", vec!["build", "--release"]),
630            FrameworkType::Axum => ("cargo", vec!["build", "--release"]),
631            FrameworkType::Rocket => ("cargo", vec!["build", "--release"]),
632            FrameworkType::YewWeb => ("trunk", vec!["build", "--release"]),
633            FrameworkType::EguiDesktop => ("cargo", vec!["build", "--release"]),
634            FrameworkType::WasmPack => ("wasm-pack", vec!["build", "--release"]),
635            FrameworkType::VanillaRust => ("cargo", vec!["build", "--release"]),
636        }
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::{Dependency, ProjectType, WorkspaceMember};
644    use std::path::PathBuf;
645
646    #[test]
647    fn test_leptos_ssr_detection() {
648        let detector = PreciseFrameworkDetector::new();
649
650        let context = ProjectContext {
651            workspace_root: PathBuf::from("/test/project"),
652            current_file: None,
653            cursor_position: None,
654            project_type: ProjectType::Binary,
655            dependencies: vec![
656                Dependency {
657                    name: "leptos".to_string(),
658                    version: "0.5.0".to_string(),
659                    features: vec![],
660                    optional: false,
661                    dev_dependency: false,
662                },
663                Dependency {
664                    name: "leptos_axum".to_string(),
665                    version: "0.5.0".to_string(),
666                    features: vec![],
667                    optional: false,
668                    dev_dependency: false,
669                },
670                Dependency {
671                    name: "axum".to_string(),
672                    version: "0.6.0".to_string(),
673                    features: vec![],
674                    optional: false,
675                    dev_dependency: false,
676                },
677            ],
678            workspace_members: vec![
679                WorkspaceMember {
680                    name: "app".to_string(),
681                    path: PathBuf::from("app"),
682                    package_type: ProjectType::Library,
683                },
684                WorkspaceMember {
685                    name: "server".to_string(),
686                    path: PathBuf::from("server"),
687                    package_type: ProjectType::Binary,
688                },
689            ],
690            build_targets: vec![],
691            active_features: vec![],
692            env_vars: std::collections::HashMap::new(),
693        };
694
695        let scores = detector.detect(&context, None);
696        assert!(!scores.is_empty());
697        assert_eq!(scores[0].framework, FrameworkType::LeptosSSR);
698        assert!(scores[0].confidence > 10.0);
699    }
700}