raz_core/
context.rs

1//! Context analysis and project structure detection
2
3use crate::error::{RazError, RazResult};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9/// Project context containing all relevant information for command generation
10#[derive(Debug, Clone)]
11pub struct ProjectContext {
12    /// Root directory of the workspace
13    pub workspace_root: PathBuf,
14
15    /// Current file being edited (if any)
16    pub current_file: Option<FileContext>,
17
18    /// Current cursor position (if available)
19    pub cursor_position: Option<Position>,
20
21    /// Detected project type
22    pub project_type: ProjectType,
23
24    /// Project dependencies
25    pub dependencies: Vec<Dependency>,
26
27    /// Workspace members (for multi-crate workspaces)
28    pub workspace_members: Vec<WorkspaceMember>,
29
30    /// Build targets found in the project
31    pub build_targets: Vec<BuildTarget>,
32
33    /// Active cargo features
34    pub active_features: Vec<String>,
35
36    /// Environment variables relevant to the project
37    pub env_vars: HashMap<String, String>,
38}
39
40/// File-specific context information
41#[derive(Debug, Clone)]
42pub struct FileContext {
43    /// Path to the file
44    pub path: PathBuf,
45
46    /// Programming language of the file
47    pub language: Language,
48
49    /// Symbols found in the file (functions, structs, etc.)
50    pub symbols: Vec<Symbol>,
51
52    /// Import statements in the file
53    pub imports: Vec<Import>,
54
55    /// Symbol at the current cursor position
56    pub cursor_symbol: Option<Symbol>,
57
58    /// Module this file belongs to
59    pub module_path: Option<String>,
60}
61
62/// Cursor position in a file
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64pub struct Position {
65    pub line: u32,
66    pub column: u32,
67}
68
69/// Range in a file
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub struct Range {
72    pub start: Position,
73    pub end: Position,
74}
75
76impl Range {
77    pub fn contains(&self, position: Position) -> bool {
78        position >= self.start && position <= self.end
79    }
80
81    pub fn contains_position(&self, position: Position) -> bool {
82        self.contains(position)
83    }
84}
85
86impl PartialOrd for Position {
87    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
88        Some(self.cmp(other))
89    }
90}
91
92impl Ord for Position {
93    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
94        self.line
95            .cmp(&other.line)
96            .then(self.column.cmp(&other.column))
97    }
98}
99
100/// Types of projects that can be detected
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub enum ProjectType {
103    Binary,
104    Library,
105    Workspace,
106    Leptos,
107    Dioxus,
108    Axum,
109    Bevy,
110    Tauri,
111    Yew,
112    Mixed(Vec<ProjectType>),
113}
114
115/// Programming language detection
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum Language {
118    Rust,
119    Toml,
120    Json,
121    Yaml,
122    Markdown,
123    Unknown,
124}
125
126impl Language {
127    pub fn from_path(path: &Path) -> Self {
128        match path.extension().and_then(|s| s.to_str()) {
129            Some("rs") => Language::Rust,
130            Some("toml") => Language::Toml,
131            Some("json") => Language::Json,
132            Some("yaml" | "yml") => Language::Yaml,
133            Some("md") => Language::Markdown,
134            _ => Language::Unknown,
135        }
136    }
137}
138
139/// Symbols found in source code
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct Symbol {
142    pub name: String,
143    pub kind: SymbolKind,
144    pub range: Range,
145    pub modifiers: Vec<String>,
146    pub children: Vec<Symbol>,
147    pub metadata: HashMap<String, String>,
148}
149
150/// Types of symbols that can be detected
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum SymbolKind {
153    Function,
154    Struct,
155    Enum,
156    Trait,
157    Module,
158    Test,
159    Impl,
160    Constant,
161    Static,
162    TypeAlias,
163    Macro,
164    Variable,
165}
166
167/// Symbol visibility (legacy - now using modifiers)
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub enum Visibility {
170    Public,
171    Private,
172    Crate,
173    Super,
174}
175
176/// Import statements
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct Import {
179    pub path: String,
180    pub alias: Option<String>,
181    pub items: Vec<String>,
182}
183
184/// Project dependency information
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct Dependency {
187    pub name: String,
188    pub version: String,
189    pub features: Vec<String>,
190    pub optional: bool,
191    pub dev_dependency: bool,
192}
193
194/// Workspace member information
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct WorkspaceMember {
197    pub name: String,
198    pub path: PathBuf,
199    pub package_type: ProjectType,
200}
201
202/// Build target information
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct BuildTarget {
205    pub name: String,
206    pub target_type: TargetType,
207    pub path: PathBuf,
208}
209
210/// Types of build targets
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum TargetType {
213    Binary,
214    Library,
215    Example,
216    Test,
217    Bench,
218}
219
220/// Enhanced test context for precise command generation
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct TestContext {
223    /// Package name for the test
224    pub package_name: Option<String>,
225    /// Target type (lib, bin, test, etc.)
226    pub target_type: TestTargetType,
227    /// Full module path to the test (e.g., ["middleware", "csrf", "tests"])
228    pub module_path: Vec<String>,
229    /// Specific test function name
230    pub test_name: Option<String>,
231    /// Required feature flags
232    pub features: Vec<String>,
233    /// Environment variables needed for the test
234    pub env_vars: Vec<(String, String)>,
235    /// Working directory for the test command
236    pub working_dir: Option<PathBuf>,
237}
238
239/// Test target types for enhanced test command generation
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub enum TestTargetType {
242    /// Library unit tests (--lib)
243    Lib,
244    /// Binary tests (--bin <name>)
245    Bin(String),
246    /// Integration tests (--test <name>)
247    Test(String),
248    /// Benchmark tests (--bench <name>)
249    Bench(String),
250    /// Example tests (--example <name>)
251    Example(String),
252}
253
254impl TestContext {
255    /// Create a new empty test context
256    pub fn new() -> Self {
257        Self {
258            package_name: None,
259            target_type: TestTargetType::Lib,
260            module_path: Vec::new(),
261            test_name: None,
262            features: Vec::new(),
263            env_vars: Vec::new(),
264            working_dir: None,
265        }
266    }
267
268    /// Get the full module path as a string (e.g., "middleware::csrf::tests")
269    pub fn module_path_string(&self) -> Option<String> {
270        if self.module_path.is_empty() {
271            None
272        } else {
273            Some(self.module_path.join("::"))
274        }
275    }
276
277    /// Get the full test path including module and test name
278    pub fn full_test_path(&self) -> Option<String> {
279        match (self.module_path_string(), &self.test_name) {
280            (Some(module), Some(test)) => Some(format!("{module}::{test}")),
281            (None, Some(test)) => Some(test.clone()),
282            (Some(module), None) => Some(module),
283            (None, None) => None,
284        }
285    }
286
287    /// Check if this context has enough information for a precise test command
288    pub fn is_precise(&self) -> bool {
289        self.test_name.is_some() || !self.module_path.is_empty()
290    }
291}
292
293impl Default for TestContext {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299/// Main project analyzer
300pub struct ProjectAnalyzer {
301    workspace_detector: WorkspaceDetector,
302    dependency_analyzer: DependencyAnalyzer,
303    target_detector: TargetDetector,
304    framework_detector: FrameworkDetector,
305    file_analyzer: FileAnalyzer,
306}
307
308impl ProjectAnalyzer {
309    pub fn new() -> Self {
310        Self {
311            workspace_detector: WorkspaceDetector::new(),
312            dependency_analyzer: DependencyAnalyzer::new(),
313            target_detector: TargetDetector::new(),
314            framework_detector: FrameworkDetector::new(),
315            file_analyzer: FileAnalyzer::new(),
316        }
317    }
318}
319
320impl Default for ProjectAnalyzer {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326impl ProjectAnalyzer {
327    /// Analyze a project and return comprehensive context
328    pub async fn analyze_project(&self, root: &Path) -> RazResult<ProjectContext> {
329        // Find Cargo.toml
330        let cargo_toml = self.find_cargo_toml(root)?;
331
332        // Analyze workspace structure
333        let mut workspace_info = self.workspace_detector.detect(&cargo_toml).await?;
334
335        // Analyze dependencies from root
336        let mut all_dependencies = self.dependency_analyzer.analyze(&cargo_toml).await?;
337
338        // If this is a workspace, analyze dependencies from all members
339        if workspace_info.is_workspace {
340            for member in &mut workspace_info.members {
341                let member_cargo_toml = root.join(&member.path).join("Cargo.toml");
342                if member_cargo_toml.exists() {
343                    // Extract the actual package name from the member's Cargo.toml
344                    if let Ok(package_name) =
345                        self.extract_package_name_from_toml(&member_cargo_toml)
346                    {
347                        member.name = package_name;
348                    }
349
350                    let member_deps = self.dependency_analyzer.analyze(&member_cargo_toml).await?;
351
352                    // Detect member's project type based on its dependencies
353                    let member_root = root.join(&member.path);
354                    member.package_type = self
355                        .framework_detector
356                        .detect(&member_deps, &[], &member_root)
357                        .await?;
358
359                    // Add member dependencies to the overall list (avoiding duplicates)
360                    for dep in member_deps {
361                        if !all_dependencies.iter().any(|d| d.name == dep.name) {
362                            all_dependencies.push(dep);
363                        }
364                    }
365                }
366            }
367        } else {
368            // For single package, ensure we have the correct package name
369            if !workspace_info.members.is_empty() {
370                if let Ok(package_name) = self.extract_package_name_from_toml(&cargo_toml) {
371                    workspace_info.members[0].name = package_name;
372                }
373            }
374        }
375
376        // Detect build targets
377        let targets = self.target_detector.detect(root).await?;
378
379        // Detect overall framework type based on all dependencies
380        let project_type = self
381            .framework_detector
382            .detect(&all_dependencies, &targets, root)
383            .await?;
384
385        Ok(ProjectContext {
386            workspace_root: root.to_path_buf(),
387            current_file: None,
388            cursor_position: None,
389            project_type,
390            dependencies: all_dependencies,
391            workspace_members: workspace_info.members,
392            build_targets: targets,
393            active_features: Vec::new(),
394            env_vars: HashMap::new(),
395        })
396    }
397
398    /// Analyze a specific file and add its context
399    pub async fn analyze_file(
400        &self,
401        context: &mut ProjectContext,
402        file_path: &Path,
403        cursor: Option<Position>,
404    ) -> RazResult<()> {
405        let file_context = self.file_analyzer.analyze_file(file_path, cursor).await?;
406        context.current_file = Some(file_context);
407        context.cursor_position = cursor;
408        Ok(())
409    }
410
411    /// Resolve test context from current project context and cursor position
412    pub async fn resolve_test_context(
413        &self,
414        context: &ProjectContext,
415    ) -> RazResult<Option<TestContext>> {
416        let Some(ref file_context) = context.current_file else {
417            return Ok(None);
418        };
419
420        // Only proceed if we're in a Rust file
421        if file_context.language != Language::Rust {
422            return Ok(None);
423        }
424
425        let mut test_context = TestContext::new();
426
427        // 1. Resolve package name
428        test_context.package_name = self.resolve_package_name(context, &file_context.path)?;
429
430        // 2. Resolve target type (lib/bin/test)
431        test_context.target_type = self.resolve_target_type(context, &file_context.path)?;
432
433        // 3. Resolve module path
434        test_context.module_path = self.resolve_module_path(&file_context.path)?;
435
436        // 4. Resolve test name if cursor is on a test function
437        if let Some(ref cursor_symbol) = file_context.cursor_symbol {
438            if cursor_symbol.kind == SymbolKind::Test
439                || (cursor_symbol.kind == SymbolKind::Function
440                    && cursor_symbol.name.starts_with("test_"))
441            {
442                test_context.test_name = Some(cursor_symbol.name.clone());
443            }
444        }
445
446        // 5. Resolve features and environment variables
447        test_context.features = self.resolve_test_features(context, &file_context.path)?;
448        test_context.env_vars = self.resolve_test_env_vars(context)?;
449
450        // 6. Set working directory
451        test_context.working_dir = Some(context.workspace_root.clone());
452
453        // Only return context if we have meaningful test information
454        if test_context.is_precise() || test_context.package_name.is_some() {
455            Ok(Some(test_context))
456        } else {
457            Ok(None)
458        }
459    }
460
461    /// Resolve package name from file path and workspace structure
462    fn resolve_package_name(
463        &self,
464        context: &ProjectContext,
465        file_path: &Path,
466    ) -> RazResult<Option<String>> {
467        // For single-package projects
468        if context.workspace_members.len() == 1 {
469            return Ok(Some(context.workspace_members[0].name.clone()));
470        }
471
472        // For workspace projects, find which member contains this file
473        for member in &context.workspace_members {
474            let member_path = if member.path.is_absolute() {
475                member.path.clone()
476            } else {
477                context.workspace_root.join(&member.path)
478            };
479
480            if file_path.starts_with(&member_path) {
481                return Ok(Some(member.name.clone()));
482            }
483        }
484
485        // Fallback: extract from the nearest Cargo.toml
486        let mut current_dir = file_path.parent();
487        while let Some(dir) = current_dir {
488            let cargo_toml = dir.join("Cargo.toml");
489            if cargo_toml.exists() {
490                if let Ok(name) = self.extract_package_name_from_toml(&cargo_toml) {
491                    return Ok(Some(name));
492                }
493            }
494            current_dir = dir.parent();
495        }
496
497        Ok(None)
498    }
499
500    /// Extract package name from Cargo.toml
501    fn extract_package_name_from_toml(&self, cargo_toml: &Path) -> RazResult<String> {
502        let content = fs::read_to_string(cargo_toml)?;
503        let parsed: toml::Value = toml::from_str(&content)
504            .map_err(|e| RazError::parse(format!("Invalid Cargo.toml: {e}")))?;
505
506        parsed
507            .get("package")
508            .and_then(|p| p.get("name"))
509            .and_then(|n| n.as_str())
510            .map(|s| s.to_string())
511            .ok_or_else(|| RazError::parse("No package name found in Cargo.toml".to_string()))
512    }
513
514    /// Resolve target type from file path
515    fn resolve_target_type(
516        &self,
517        context: &ProjectContext,
518        file_path: &Path,
519    ) -> RazResult<TestTargetType> {
520        let file_str = file_path.to_string_lossy();
521
522        // Check for integration tests (tests/ directory)
523        if file_str.contains("/tests/") {
524            if let Some(test_name) = file_path.file_stem().and_then(|s| s.to_str()) {
525                return Ok(TestTargetType::Test(test_name.to_string()));
526            }
527        }
528
529        // Check for examples (examples/ directory)
530        if file_str.contains("/examples/") {
531            if let Some(example_name) = file_path.file_stem().and_then(|s| s.to_str()) {
532                return Ok(TestTargetType::Example(example_name.to_string()));
533            }
534        }
535
536        // Check for benchmarks (benches/ directory)
537        if file_str.contains("/benches/") {
538            if let Some(bench_name) = file_path.file_stem().and_then(|s| s.to_str()) {
539                return Ok(TestTargetType::Bench(bench_name.to_string()));
540            }
541        }
542
543        // Check if it's a library file
544        if file_str.contains("/src/lib.rs") || file_str.contains("/src/mod.rs") {
545            return Ok(TestTargetType::Lib);
546        }
547
548        // Check for binary files
549        if file_str.contains("/src/main.rs") {
550            // Main binary
551            return Ok(TestTargetType::Bin("main".to_string()));
552        }
553
554        if file_str.contains("/src/bin/") {
555            if let Some(bin_name) = file_path.file_stem().and_then(|s| s.to_str()) {
556                return Ok(TestTargetType::Bin(bin_name.to_string()));
557            }
558        }
559
560        // Check against build targets for more precise detection
561        for target in &context.build_targets {
562            if file_path.starts_with(target.path.parent().unwrap_or(&context.workspace_root)) {
563                return match target.target_type {
564                    TargetType::Binary => Ok(TestTargetType::Bin(target.name.clone())),
565                    TargetType::Library => Ok(TestTargetType::Lib),
566                    TargetType::Test => Ok(TestTargetType::Test(target.name.clone())),
567                    TargetType::Bench => Ok(TestTargetType::Bench(target.name.clone())),
568                    TargetType::Example => Ok(TestTargetType::Example(target.name.clone())),
569                };
570            }
571        }
572
573        // Default to library
574        Ok(TestTargetType::Lib)
575    }
576
577    /// Resolve module path from file path using multiple strategies
578    fn resolve_module_path(&self, file_path: &Path) -> RazResult<Vec<String>> {
579        // Strategy 1: Tree-sitter AST analysis (most precise)
580        if let Ok(module_path) = self.resolve_module_path_treesitter(file_path) {
581            if !module_path.is_empty() {
582                return Ok(module_path);
583            }
584        }
585
586        // Strategy 2: Rust Analyzer LSP integration (if available)
587        if let Ok(module_path) = self.resolve_module_path_rust_analyzer(file_path) {
588            if !module_path.is_empty() {
589                return Ok(module_path);
590            }
591        }
592
593        // Strategy 3: Basic file path analysis (fallback)
594        if let Some(module_str) = FileAnalyzer::extract_module_path(file_path) {
595            Ok(module_str
596                .split("::")
597                .filter(|s| !s.is_empty())
598                .map(|s| s.to_string())
599                .collect())
600        } else {
601            Ok(Vec::new())
602        }
603    }
604
605    /// Resolve module path using tree-sitter AST analysis
606    fn resolve_module_path_treesitter(&self, file_path: &Path) -> RazResult<Vec<String>> {
607        // Only proceed if tree-sitter is available
608        if self.file_analyzer.rust_analyzer.is_none() {
609            return Ok(Vec::new());
610        }
611
612        let content = fs::read_to_string(file_path)?;
613        let mut rust_analyzer = crate::ast::RustAnalyzer::new()?;
614        let tree = rust_analyzer.parse(&content)?;
615
616        // 1. Start with file-based module path
617        let mut module_path = Vec::new();
618        if let Some(base_path) = FileAnalyzer::extract_module_path(file_path) {
619            module_path = base_path
620                .split("::")
621                .filter(|s| !s.is_empty())
622                .map(|s| s.to_string())
623                .collect();
624        }
625
626        // 2. Look for nested module declarations and test modules
627        let symbols = rust_analyzer.extract_symbols(&tree, &content)?;
628
629        // Find test modules (common pattern: mod tests { ... })
630        let test_modules: Vec<_> = symbols
631            .iter()
632            .filter(|s| {
633                s.kind == SymbolKind::Module && (s.name == "tests" || s.name.contains("test"))
634            })
635            .collect();
636
637        // If we're in a test module, add it to the path
638        if !test_modules.is_empty() {
639            // For now, assume we're in the first test module found
640            // TODO: Use cursor position to determine exact module when available
641            if let Some(test_mod) = test_modules.first() {
642                module_path.push(test_mod.name.clone());
643            }
644        }
645
646        // 3. Enhance path detection based on common Rust patterns
647        module_path = self.enhance_module_path_with_patterns(module_path, file_path, &content)?;
648
649        Ok(module_path)
650    }
651
652    /// Enhance module path detection with common Rust patterns
653    fn enhance_module_path_with_patterns(
654        &self,
655        mut module_path: Vec<String>,
656        file_path: &Path,
657        content: &str,
658    ) -> RazResult<Vec<String>> {
659        let file_str = file_path.to_string_lossy();
660
661        // Pattern 1: Test files often have tests module
662        if (content.contains("#[cfg(test)]") || content.contains("mod tests"))
663            && !module_path.iter().any(|m| m == "tests")
664        {
665            module_path.push("tests".to_string());
666        }
667
668        // Pattern 2: Integration tests in tests/ directory
669        if file_str.contains("/tests/") {
670            // Integration tests don't typically have nested module paths
671            // The test file name becomes the module
672            if let Some(test_name) = file_path.file_stem().and_then(|s| s.to_str()) {
673                module_path = vec![test_name.to_string()];
674            }
675        }
676
677        // Pattern 3: Common module patterns (middleware, handlers, etc.)
678        let common_modules = [
679            "middleware",
680            "handlers",
681            "controllers",
682            "services",
683            "utils",
684            "helpers",
685        ];
686        for common in &common_modules {
687            if file_str.contains(&format!("/{common}/"))
688                && !module_path.contains(&common.to_string())
689            {
690                // Find the position to insert this module
691                if let Some(pos) = module_path.iter().position(|m| m == "tests") {
692                    module_path.insert(pos, common.to_string());
693                } else {
694                    module_path.insert(
695                        module_path.len().saturating_sub(1).max(0),
696                        common.to_string(),
697                    );
698                }
699            }
700        }
701
702        Ok(module_path)
703    }
704
705    /// Resolve module path using rust-analyzer LSP integration
706    fn resolve_module_path_rust_analyzer(&self, _file_path: &Path) -> RazResult<Vec<String>> {
707        // TODO: Implement rust-analyzer LSP integration
708        // This would involve:
709        // 1. Starting/connecting to rust-analyzer LSP server
710        // 2. Sending textDocument/documentSymbol request
711        // 3. Parsing the response to extract module hierarchy
712        // 4. Mapping cursor position to exact module path
713
714        // For now, return empty to fall back to other strategies
715        Ok(Vec::new())
716    }
717
718    /// Resolve features required for testing this file
719    fn resolve_test_features(
720        &self,
721        context: &ProjectContext,
722        _file_path: &Path,
723    ) -> RazResult<Vec<String>> {
724        // For now, return active features from the project context
725        // TODO: In the future, we could parse #[cfg(feature = "...")] attributes from the file
726        Ok(context.active_features.clone())
727    }
728
729    /// Resolve environment variables needed for testing
730    fn resolve_test_env_vars(&self, context: &ProjectContext) -> RazResult<Vec<(String, String)>> {
731        Ok(context
732            .env_vars
733            .iter()
734            .map(|(k, v)| (k.clone(), v.clone()))
735            .collect())
736    }
737
738    fn find_cargo_toml(&self, root: &Path) -> RazResult<PathBuf> {
739        let cargo_toml = root.join("Cargo.toml");
740        if cargo_toml.exists() {
741            Ok(cargo_toml)
742        } else {
743            Err(RazError::invalid_workspace(root))
744        }
745    }
746}
747
748/// Workspace detection and analysis
749pub struct WorkspaceDetector;
750
751impl WorkspaceDetector {
752    pub fn new() -> Self {
753        Self
754    }
755}
756
757impl Default for WorkspaceDetector {
758    fn default() -> Self {
759        Self::new()
760    }
761}
762
763impl WorkspaceDetector {
764    pub async fn detect(&self, cargo_toml: &Path) -> RazResult<WorkspaceInfo> {
765        let content = fs::read_to_string(cargo_toml)?;
766        let parsed: toml::Value = toml::from_str(&content)
767            .map_err(|e| RazError::parse(format!("Invalid Cargo.toml: {e}")))?;
768
769        if let Some(workspace) = parsed.get("workspace") {
770            let members = self.extract_workspace_members(workspace)?;
771            Ok(WorkspaceInfo {
772                is_workspace: true,
773                members,
774            })
775        } else {
776            // Single package
777            let package_name = parsed
778                .get("package")
779                .and_then(|p| p.get("name"))
780                .and_then(|n| n.as_str())
781                .unwrap_or("unknown")
782                .to_string();
783
784            Ok(WorkspaceInfo {
785                is_workspace: false,
786                members: vec![WorkspaceMember {
787                    name: package_name,
788                    path: cargo_toml.parent().unwrap().to_path_buf(),
789                    package_type: ProjectType::Binary, // Will be refined later
790                }],
791            })
792        }
793    }
794
795    fn extract_workspace_members(
796        &self,
797        workspace: &toml::Value,
798    ) -> RazResult<Vec<WorkspaceMember>> {
799        let members = workspace
800            .get("members")
801            .and_then(|m| m.as_array())
802            .ok_or_else(|| RazError::parse("Workspace missing members"))?;
803
804        let mut result = Vec::new();
805
806        for member in members {
807            if let Some(path_str) = member.as_str() {
808                result.push(WorkspaceMember {
809                    name: path_str.to_string(), // Will be updated with actual package name
810                    path: PathBuf::from(path_str),
811                    package_type: ProjectType::Binary, // Will be refined later
812                });
813            }
814        }
815
816        Ok(result)
817    }
818}
819
820#[derive(Debug)]
821pub struct WorkspaceInfo {
822    pub is_workspace: bool,
823    pub members: Vec<WorkspaceMember>,
824}
825
826/// Dependency analysis
827pub struct DependencyAnalyzer;
828
829impl DependencyAnalyzer {
830    pub fn new() -> Self {
831        Self
832    }
833}
834
835impl Default for DependencyAnalyzer {
836    fn default() -> Self {
837        Self::new()
838    }
839}
840
841impl DependencyAnalyzer {
842    pub async fn analyze(&self, cargo_toml: &Path) -> RazResult<Vec<Dependency>> {
843        let content = fs::read_to_string(cargo_toml)?;
844        let parsed: toml::Value = toml::from_str(&content)
845            .map_err(|e| RazError::parse(format!("Invalid Cargo.toml: {e}")))?;
846
847        let mut dependencies = Vec::new();
848
849        // Regular dependencies
850        if let Some(deps) = parsed.get("dependencies").and_then(|d| d.as_table()) {
851            dependencies.extend(self.parse_dependencies(deps, false)?);
852        }
853
854        // Dev dependencies
855        if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
856            dependencies.extend(self.parse_dependencies(dev_deps, true)?);
857        }
858
859        Ok(dependencies)
860    }
861
862    fn parse_dependencies(
863        &self,
864        deps: &toml::value::Table,
865        is_dev: bool,
866    ) -> RazResult<Vec<Dependency>> {
867        let mut result = Vec::new();
868
869        for (name, value) in deps {
870            let dependency = match value {
871                toml::Value::String(version) => Dependency {
872                    name: name.clone(),
873                    version: version.clone(),
874                    features: Vec::new(),
875                    optional: false,
876                    dev_dependency: is_dev,
877                },
878                toml::Value::Table(table) => {
879                    let version = table
880                        .get("version")
881                        .and_then(|v| v.as_str())
882                        .unwrap_or("*")
883                        .to_string();
884
885                    let features = table
886                        .get("features")
887                        .and_then(|f| f.as_array())
888                        .map(|arr| {
889                            arr.iter()
890                                .filter_map(|v| v.as_str())
891                                .map(|s| s.to_string())
892                                .collect()
893                        })
894                        .unwrap_or_default();
895
896                    let optional = table
897                        .get("optional")
898                        .and_then(|o| o.as_bool())
899                        .unwrap_or(false);
900
901                    Dependency {
902                        name: name.clone(),
903                        version,
904                        features,
905                        optional,
906                        dev_dependency: is_dev,
907                    }
908                }
909                _ => continue,
910            };
911
912            result.push(dependency);
913        }
914
915        Ok(result)
916    }
917}
918
919/// Build target detection
920pub struct TargetDetector;
921
922impl TargetDetector {
923    pub fn new() -> Self {
924        Self
925    }
926}
927
928impl Default for TargetDetector {
929    fn default() -> Self {
930        Self::new()
931    }
932}
933
934impl TargetDetector {
935    pub async fn detect(&self, root: &Path) -> RazResult<Vec<BuildTarget>> {
936        let mut targets = Vec::new();
937
938        // Check for main.rs (binary)
939        let main_rs = root.join("src/main.rs");
940        if main_rs.exists() {
941            targets.push(BuildTarget {
942                name: "main".to_string(),
943                target_type: TargetType::Binary,
944                path: main_rs,
945            });
946        }
947
948        // Check for lib.rs (library)
949        let lib_rs = root.join("src/lib.rs");
950        if lib_rs.exists() {
951            targets.push(BuildTarget {
952                name: "lib".to_string(),
953                target_type: TargetType::Library,
954                path: lib_rs,
955            });
956        }
957
958        // Check examples directory
959        let examples_dir = root.join("examples");
960        if examples_dir.exists() {
961            for entry in fs::read_dir(&examples_dir)? {
962                let entry = entry?;
963                if let Some(name) = entry.file_name().to_str() {
964                    if name.ends_with(".rs") {
965                        let name = name.strip_suffix(".rs").unwrap();
966                        targets.push(BuildTarget {
967                            name: name.to_string(),
968                            target_type: TargetType::Example,
969                            path: entry.path(),
970                        });
971                    }
972                }
973            }
974        }
975
976        // Check tests directory
977        let tests_dir = root.join("tests");
978        if tests_dir.exists() {
979            for entry in fs::read_dir(&tests_dir)? {
980                let entry = entry?;
981                if let Some(name) = entry.file_name().to_str() {
982                    if name.ends_with(".rs") {
983                        let name = name.strip_suffix(".rs").unwrap();
984                        targets.push(BuildTarget {
985                            name: name.to_string(),
986                            target_type: TargetType::Test,
987                            path: entry.path(),
988                        });
989                    }
990                }
991            }
992        }
993
994        Ok(targets)
995    }
996}
997
998/// Framework detection based on dependencies and project structure
999pub struct FrameworkDetector {
1000    rules: Vec<DetectionRule>,
1001}
1002
1003impl FrameworkDetector {
1004    pub fn new() -> Self {
1005        Self {
1006            rules: Self::default_rules(),
1007        }
1008    }
1009}
1010
1011impl Default for FrameworkDetector {
1012    fn default() -> Self {
1013        Self::new()
1014    }
1015}
1016
1017impl FrameworkDetector {
1018    pub async fn detect(
1019        &self,
1020        dependencies: &[Dependency],
1021        _targets: &[BuildTarget],
1022        workspace_root: &Path,
1023    ) -> RazResult<ProjectType> {
1024        let mut detected_frameworks = Vec::new();
1025
1026        for rule in &self.rules {
1027            if rule.matches(dependencies, workspace_root) {
1028                // Check if we already detected this framework type
1029                if !detected_frameworks.contains(&rule.project_type) {
1030                    detected_frameworks.push(rule.project_type.clone());
1031                }
1032            }
1033        }
1034
1035        match detected_frameworks.len() {
1036            0 => Ok(ProjectType::Binary), // Default fallback
1037            1 => Ok(detected_frameworks.into_iter().next().unwrap()),
1038            _ => {
1039                // Multiple frameworks detected - create a mixed project type
1040                // Sort frameworks by priority (Tauri first as it's often the wrapper)
1041                detected_frameworks.sort_by_key(|framework| match framework {
1042                    ProjectType::Tauri => 0,  // Highest priority - desktop wrapper
1043                    ProjectType::Leptos => 1, // Web framework
1044                    ProjectType::Dioxus => 2, // Cross-platform UI
1045                    ProjectType::Yew => 3,    // Web framework
1046                    ProjectType::Bevy => 4,   // Game engine
1047                    ProjectType::Axum => 5,   // Web server
1048                    _ => 6,                   // Other types
1049                });
1050                Ok(ProjectType::Mixed(detected_frameworks))
1051            }
1052        }
1053    }
1054
1055    fn default_rules() -> Vec<DetectionRule> {
1056        vec![
1057            // Tauri detection (highest priority due to specific structure)
1058            DetectionRule {
1059                framework: "tauri".to_string(),
1060                project_type: ProjectType::Tauri,
1061                conditions: vec![
1062                    DetectionCondition::HasFilePattern("src-tauri/Cargo.toml".to_string()),
1063                    DetectionCondition::HasFilePattern("src-tauri/tauri.conf.json".to_string()),
1064                ],
1065            },
1066            // Alternative Tauri detection by dependency
1067            DetectionRule {
1068                framework: "tauri".to_string(),
1069                project_type: ProjectType::Tauri,
1070                conditions: vec![DetectionCondition::HasDependency("tauri".to_string())],
1071            },
1072            // Leptos detection with configuration file
1073            DetectionRule {
1074                framework: "leptos".to_string(),
1075                project_type: ProjectType::Leptos,
1076                conditions: vec![
1077                    DetectionCondition::HasAnyDependency(vec![
1078                        "leptos".to_string(),
1079                        "leptos_axum".to_string(),
1080                        "leptos_actix".to_string(),
1081                        "leptos_router".to_string(),
1082                        "leptos_reactive".to_string(),
1083                    ]),
1084                    DetectionCondition::ConfigFileContains(
1085                        PathBuf::from("Cargo.toml"),
1086                        "[package.metadata.leptos]".to_string(),
1087                    ),
1088                ],
1089            },
1090            // Leptos detection by dependency only (fallback)
1091            DetectionRule {
1092                framework: "leptos".to_string(),
1093                project_type: ProjectType::Leptos,
1094                conditions: vec![DetectionCondition::HasAnyDependency(vec![
1095                    "leptos".to_string(),
1096                    "leptos_axum".to_string(),
1097                    "leptos_actix".to_string(),
1098                    "leptos_router".to_string(),
1099                    "leptos_reactive".to_string(),
1100                ])],
1101            },
1102            // Dioxus detection with configuration file
1103            DetectionRule {
1104                framework: "dioxus".to_string(),
1105                project_type: ProjectType::Dioxus,
1106                conditions: vec![
1107                    DetectionCondition::HasDependency("dioxus".to_string()),
1108                    DetectionCondition::HasFile(PathBuf::from("Dioxus.toml")),
1109                ],
1110            },
1111            // Dioxus detection by dependency only (fallback)
1112            DetectionRule {
1113                framework: "dioxus".to_string(),
1114                project_type: ProjectType::Dioxus,
1115                conditions: vec![DetectionCondition::HasAnyDependency(vec![
1116                    "dioxus".to_string(),
1117                    "dioxus-web".to_string(),
1118                    "dioxus-desktop".to_string(),
1119                    "dioxus-mobile".to_string(),
1120                ])],
1121            },
1122            // Bevy detection
1123            DetectionRule {
1124                framework: "bevy".to_string(),
1125                project_type: ProjectType::Bevy,
1126                conditions: vec![DetectionCondition::HasDependency("bevy".to_string())],
1127            },
1128            // Axum detection
1129            DetectionRule {
1130                framework: "axum".to_string(),
1131                project_type: ProjectType::Axum,
1132                conditions: vec![DetectionCondition::HasDependency("axum".to_string())],
1133            },
1134            // Yew detection with Trunk configuration
1135            DetectionRule {
1136                framework: "yew".to_string(),
1137                project_type: ProjectType::Yew,
1138                conditions: vec![
1139                    DetectionCondition::HasDependency("yew".to_string()),
1140                    DetectionCondition::HasFile(PathBuf::from("Trunk.toml")),
1141                ],
1142            },
1143            // Yew detection by dependency only (fallback)
1144            DetectionRule {
1145                framework: "yew".to_string(),
1146                project_type: ProjectType::Yew,
1147                conditions: vec![DetectionCondition::HasAnyDependency(vec![
1148                    "yew".to_string(),
1149                    "yew-router".to_string(),
1150                    "yew-hooks".to_string(),
1151                ])],
1152            },
1153        ]
1154    }
1155}
1156
1157#[derive(Debug)]
1158pub struct DetectionRule {
1159    pub framework: String,
1160    pub project_type: ProjectType,
1161    pub conditions: Vec<DetectionCondition>,
1162}
1163
1164impl DetectionRule {
1165    pub fn matches(&self, dependencies: &[Dependency], workspace_root: &Path) -> bool {
1166        self.conditions
1167            .iter()
1168            .all(|condition| condition.is_met(dependencies, workspace_root))
1169    }
1170}
1171
1172#[derive(Debug)]
1173pub enum DetectionCondition {
1174    HasDependency(String),
1175    HasAnyDependency(Vec<String>),
1176    HasFile(PathBuf),
1177    HasFilePattern(String),
1178    ConfigFileContains(PathBuf, String),
1179}
1180
1181impl DetectionCondition {
1182    pub fn is_met(&self, dependencies: &[Dependency], workspace_root: &Path) -> bool {
1183        match self {
1184            DetectionCondition::HasDependency(name) => {
1185                dependencies.iter().any(|dep| dep.name == *name)
1186            }
1187            DetectionCondition::HasAnyDependency(names) => {
1188                dependencies.iter().any(|dep| names.contains(&dep.name))
1189            }
1190            DetectionCondition::HasFile(path) => path.exists(),
1191            DetectionCondition::HasFilePattern(pattern) => {
1192                Self::check_file_pattern(workspace_root, pattern)
1193            }
1194            DetectionCondition::ConfigFileContains(path, content) => {
1195                Self::check_file_content(workspace_root, path, content)
1196            }
1197        }
1198    }
1199
1200    /// Check if any files matching the given pattern exist in the workspace
1201    fn check_file_pattern(workspace_root: &Path, pattern: &str) -> bool {
1202        // Create the full pattern by joining with workspace root
1203        let full_pattern = workspace_root.join(pattern);
1204        let pattern_str = full_pattern.to_string_lossy();
1205
1206        // Use glob to find matching files
1207        match glob::glob(&pattern_str) {
1208            Ok(paths) => {
1209                // Check if any paths match
1210                paths.filter_map(Result::ok).next().is_some()
1211            }
1212            Err(_) => false,
1213        }
1214    }
1215
1216    /// Check if a file contains specific content
1217    fn check_file_content(workspace_root: &Path, file_path: &Path, content: &str) -> bool {
1218        let full_path = if file_path.is_absolute() {
1219            file_path.to_path_buf()
1220        } else {
1221            workspace_root.join(file_path)
1222        };
1223
1224        match std::fs::read_to_string(&full_path) {
1225            Ok(file_content) => file_content.contains(content),
1226            Err(_) => false,
1227        }
1228    }
1229}
1230
1231/// File analysis using tree-sitter
1232pub struct FileAnalyzer {
1233    rust_analyzer: Option<crate::ast::RustAnalyzer>,
1234}
1235
1236impl Default for FileAnalyzer {
1237    fn default() -> Self {
1238        Self::new()
1239    }
1240}
1241
1242impl FileAnalyzer {
1243    pub fn new() -> Self {
1244        let rust_analyzer = crate::ast::RustAnalyzer::new().ok();
1245        Self { rust_analyzer }
1246    }
1247
1248    pub async fn analyze_file(
1249        &self,
1250        path: &Path,
1251        cursor: Option<Position>,
1252    ) -> RazResult<FileContext> {
1253        let language = Language::from_path(path);
1254
1255        if language == Language::Rust && self.rust_analyzer.is_some() {
1256            self.analyze_rust_file(path, cursor).await
1257        } else {
1258            // Fallback for non-Rust files or if tree-sitter fails
1259            Ok(FileContext {
1260                path: path.to_path_buf(),
1261                language,
1262                symbols: Vec::new(),
1263                imports: Vec::new(),
1264                cursor_symbol: None,
1265                module_path: Self::extract_module_path(path),
1266            })
1267        }
1268    }
1269
1270    async fn analyze_rust_file(
1271        &self,
1272        path: &Path,
1273        cursor: Option<Position>,
1274    ) -> RazResult<FileContext> {
1275        if let Some(ref _analyzer) = self.rust_analyzer {
1276            let content = fs::read_to_string(path)?;
1277            let mut rust_analyzer = crate::ast::RustAnalyzer::new()?;
1278            let tree = rust_analyzer.parse(&content)?;
1279
1280            // Extract symbols
1281            let symbols = rust_analyzer.extract_symbols(&tree, &content)?;
1282
1283            // Find cursor symbol if position is provided
1284            let cursor_symbol = if let Some(pos) = cursor {
1285                rust_analyzer.symbol_at_position(&tree, &content, pos)?
1286            } else {
1287                None
1288            };
1289
1290            // Extract imports (basic implementation)
1291            let imports = self.extract_imports(&content);
1292
1293            Ok(FileContext {
1294                path: path.to_path_buf(),
1295                language: Language::Rust,
1296                symbols,
1297                imports,
1298                cursor_symbol,
1299                module_path: Self::extract_module_path(path),
1300            })
1301        } else {
1302            Err(RazError::analysis(
1303                "Rust analyzer not available".to_string(),
1304            ))
1305        }
1306    }
1307
1308    fn extract_imports(&self, content: &str) -> Vec<Import> {
1309        let mut imports = Vec::new();
1310
1311        for line in content.lines() {
1312            let trimmed = line.trim();
1313            if trimmed.starts_with("use ") {
1314                if let Some(import) = self.parse_use_statement(trimmed) {
1315                    imports.push(import);
1316                }
1317            }
1318        }
1319
1320        imports
1321    }
1322
1323    fn parse_use_statement(&self, use_line: &str) -> Option<Import> {
1324        let use_part = use_line.strip_prefix("use ")?.strip_suffix(";")?;
1325
1326        // Handle glob imports: use module::*;
1327        if use_part.ends_with("::*") {
1328            let path = use_part.strip_suffix("::*")?.to_string();
1329            return Some(Import {
1330                path,
1331                alias: None,
1332                items: vec!["*".to_string()],
1333            });
1334        }
1335
1336        // Handle braced imports first: use path::{item1, item2 as alias, item3};
1337        // Check for braces before checking for simple aliases
1338        if let Some(brace_start) = use_part.find("::{") {
1339            if let Some(brace_end) = use_part.rfind('}') {
1340                let path = use_part[..brace_start].trim().to_string();
1341                let items_str = &use_part[brace_start + 3..brace_end];
1342                let items = self.parse_import_items(items_str);
1343
1344                return Some(Import {
1345                    path,
1346                    alias: None,
1347                    items,
1348                });
1349            }
1350        }
1351
1352        // Handle simple alias: use path as alias;
1353        if let Some(as_pos) = use_part.find(" as ") {
1354            let path = use_part[..as_pos].trim();
1355            let alias = use_part[as_pos + 4..].trim();
1356            return Some(Import {
1357                path: path.to_string(),
1358                alias: Some(alias.to_string()),
1359                items: vec![],
1360            });
1361        }
1362
1363        // Simple import: use path;
1364        Some(Import {
1365            path: use_part.to_string(),
1366            alias: None,
1367            items: vec![],
1368        })
1369    }
1370
1371    /// Parse comma-separated import items, handling aliases and nested groups
1372    fn parse_import_items(&self, items_str: &str) -> Vec<String> {
1373        let mut items = Vec::new();
1374        let mut current_item = String::new();
1375        let mut brace_depth = 0;
1376
1377        for ch in items_str.chars() {
1378            match ch {
1379                '{' => {
1380                    brace_depth += 1;
1381                    current_item.push(ch);
1382                }
1383                '}' => {
1384                    brace_depth -= 1;
1385                    current_item.push(ch);
1386                }
1387                ',' if brace_depth == 0 => {
1388                    let item = current_item.trim();
1389                    if !item.is_empty() {
1390                        items.push(item.to_string());
1391                    }
1392                    current_item.clear();
1393                }
1394                _ => current_item.push(ch),
1395            }
1396        }
1397
1398        // Add the last item
1399        let item = current_item.trim();
1400        if !item.is_empty() {
1401            items.push(item.to_string());
1402        }
1403
1404        items
1405    }
1406
1407    pub fn extract_module_path(path: &Path) -> Option<String> {
1408        // Extract module path from file path
1409        // e.g., src/handlers/auth.rs -> handlers::auth
1410        if let Some(src_index) = path.to_str()?.find("src/") {
1411            let relative_path = &path.to_str()?[src_index + 4..];
1412            let without_extension = relative_path.strip_suffix(".rs")?;
1413            let module_path = without_extension.replace('/', "::").replace("main", "");
1414            if module_path.is_empty() {
1415                None
1416            } else {
1417                Some(module_path)
1418            }
1419        } else {
1420            None
1421        }
1422    }
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427    use super::*;
1428    use tempfile::TempDir;
1429
1430    #[tokio::test]
1431    async fn test_project_analysis() {
1432        let temp_dir = TempDir::new().unwrap();
1433        let cargo_toml = temp_dir.path().join("Cargo.toml");
1434        fs::write(
1435            &cargo_toml,
1436            r#"
1437            [package]
1438            name = "test-project"
1439            version = "0.1.0"
1440            edition = "2021"
1441            
1442            [dependencies]
1443            leptos = "0.5"
1444        "#,
1445        )
1446        .unwrap();
1447
1448        let analyzer = ProjectAnalyzer::new();
1449        let context = analyzer.analyze_project(temp_dir.path()).await.unwrap();
1450
1451        assert_eq!(context.workspace_root, temp_dir.path());
1452        assert_eq!(context.project_type, ProjectType::Leptos);
1453        assert!(context.dependencies.iter().any(|d| d.name == "leptos"));
1454    }
1455
1456    #[test]
1457    fn test_language_detection() {
1458        assert_eq!(Language::from_path(Path::new("main.rs")), Language::Rust);
1459        assert_eq!(Language::from_path(Path::new("Cargo.toml")), Language::Toml);
1460        assert_eq!(
1461            Language::from_path(Path::new("package.json")),
1462            Language::Json
1463        );
1464    }
1465
1466    #[test]
1467    fn test_position_ordering() {
1468        let pos1 = Position { line: 1, column: 5 };
1469        let pos2 = Position { line: 2, column: 3 };
1470        let pos3 = Position {
1471            line: 1,
1472            column: 10,
1473        };
1474
1475        assert!(pos1 < pos2);
1476        assert!(pos1 < pos3);
1477        assert!(pos3 < pos2);
1478    }
1479
1480    #[test]
1481    fn test_range_contains() {
1482        let range = Range {
1483            start: Position { line: 1, column: 0 },
1484            end: Position {
1485                line: 3,
1486                column: 10,
1487            },
1488        };
1489
1490        assert!(range.contains(Position { line: 2, column: 5 }));
1491        assert!(range.contains(Position { line: 1, column: 0 }));
1492        assert!(range.contains(Position {
1493            line: 3,
1494            column: 10
1495        }));
1496        assert!(!range.contains(Position { line: 0, column: 5 }));
1497        assert!(!range.contains(Position { line: 4, column: 5 }));
1498    }
1499
1500    #[test]
1501    fn test_file_pattern_detection() {
1502        let temp_dir = TempDir::new().unwrap();
1503        let temp_path = temp_dir.path();
1504
1505        // Create test files
1506        std::fs::create_dir_all(temp_path.join("src-tauri")).unwrap();
1507        std::fs::write(temp_path.join("src-tauri/Cargo.toml"), "").unwrap();
1508        std::fs::write(temp_path.join("src-tauri/tauri.conf.json"), "{}").unwrap();
1509        std::fs::write(temp_path.join("Dioxus.toml"), "[application]").unwrap();
1510
1511        // Test pattern matching
1512        assert!(DetectionCondition::check_file_pattern(
1513            temp_path,
1514            "src-tauri/Cargo.toml"
1515        ));
1516        assert!(DetectionCondition::check_file_pattern(
1517            temp_path,
1518            "src-tauri/*.json"
1519        ));
1520        assert!(DetectionCondition::check_file_pattern(
1521            temp_path,
1522            "Dioxus.toml"
1523        ));
1524        assert!(!DetectionCondition::check_file_pattern(
1525            temp_path,
1526            "nonexistent.toml"
1527        ));
1528        assert!(!DetectionCondition::check_file_pattern(
1529            temp_path,
1530            "src-nonexistent/*.toml"
1531        ));
1532    }
1533
1534    #[test]
1535    fn test_file_content_detection() {
1536        let temp_dir = TempDir::new().unwrap();
1537        let temp_path = temp_dir.path();
1538
1539        // Create test file with content
1540        let cargo_toml_content = r#"
1541[package]
1542name = "test"
1543
1544[package.metadata.leptos]
1545output-name = "my-app"
1546        "#;
1547        std::fs::write(temp_path.join("Cargo.toml"), cargo_toml_content).unwrap();
1548
1549        // Test content checking
1550        assert!(DetectionCondition::check_file_content(
1551            temp_path,
1552            &PathBuf::from("Cargo.toml"),
1553            "[package.metadata.leptos]"
1554        ));
1555        assert!(DetectionCondition::check_file_content(
1556            temp_path,
1557            &PathBuf::from("Cargo.toml"),
1558            "output-name"
1559        ));
1560        assert!(!DetectionCondition::check_file_content(
1561            temp_path,
1562            &PathBuf::from("Cargo.toml"),
1563            "nonexistent-content"
1564        ));
1565        assert!(!DetectionCondition::check_file_content(
1566            temp_path,
1567            &PathBuf::from("nonexistent.toml"),
1568            "any-content"
1569        ));
1570    }
1571
1572    #[tokio::test]
1573    async fn test_multi_framework_detection() {
1574        let temp_dir = TempDir::new().unwrap();
1575        let temp_path = temp_dir.path();
1576
1577        // Create a mixed Tauri + Leptos project structure
1578        std::fs::create_dir_all(temp_path.join("src-tauri")).unwrap();
1579        std::fs::write(temp_path.join("src-tauri/Cargo.toml"), "").unwrap();
1580        std::fs::write(temp_path.join("src-tauri/tauri.conf.json"), "{}").unwrap();
1581
1582        let cargo_toml_content = r#"
1583[package]
1584name = "mixed-app"
1585
1586[package.metadata.leptos]
1587output-name = "my-app"
1588
1589[dependencies]
1590leptos = "0.6"
1591tauri = "1.0"
1592        "#;
1593        std::fs::write(temp_path.join("Cargo.toml"), cargo_toml_content).unwrap();
1594
1595        // Test framework detection
1596        let detector = FrameworkDetector::new();
1597        let deps = vec![
1598            Dependency {
1599                name: "leptos".to_string(),
1600                version: "0.6".to_string(),
1601                features: vec![],
1602                optional: false,
1603                dev_dependency: false,
1604            },
1605            Dependency {
1606                name: "tauri".to_string(),
1607                version: "1.0".to_string(),
1608                features: vec![],
1609                optional: false,
1610                dev_dependency: false,
1611            },
1612        ];
1613
1614        let result = detector.detect(&deps, &[], temp_path).await.unwrap();
1615
1616        match result {
1617            ProjectType::Mixed(frameworks) => {
1618                assert!(frameworks.contains(&ProjectType::Tauri));
1619                assert!(frameworks.contains(&ProjectType::Leptos));
1620                // Tauri should be first (higher priority)
1621                assert_eq!(frameworks[0], ProjectType::Tauri);
1622            }
1623            _ => panic!("Expected Mixed project type, got {result:?}"),
1624        }
1625    }
1626
1627    #[test]
1628    fn test_complex_use_statement_parsing() {
1629        let analyzer = FileAnalyzer::new();
1630
1631        // Test glob import
1632        let import = analyzer.parse_use_statement("use std::*;").unwrap();
1633        assert_eq!(import.path, "std");
1634        assert_eq!(import.items, vec!["*"]);
1635
1636        // Test simple alias
1637        let import = analyzer
1638            .parse_use_statement("use std::collections::HashMap as Map;")
1639            .unwrap();
1640        assert_eq!(import.path, "std::collections::HashMap");
1641        assert_eq!(import.alias, Some("Map".to_string()));
1642
1643        // Test braced imports
1644        let import = analyzer
1645            .parse_use_statement("use std::{fs, io, collections::HashMap};")
1646            .unwrap();
1647        assert_eq!(import.path, "std");
1648        assert_eq!(import.items, vec!["fs", "io", "collections::HashMap"]);
1649
1650        // Test complex braced imports with aliases
1651        let import = analyzer
1652            .parse_use_statement("use crate::module::{Item1, Item2 as Alias, Item3};")
1653            .unwrap();
1654        assert_eq!(import.path, "crate::module");
1655        assert_eq!(import.items, vec!["Item1", "Item2 as Alias", "Item3"]);
1656
1657        // Test nested braces (complex case)
1658        let import = analyzer
1659            .parse_use_statement("use std::{fs, io::{self, Read, Write}};")
1660            .unwrap();
1661        assert_eq!(import.path, "std");
1662        assert_eq!(import.items, vec!["fs", "io::{self, Read, Write}"]);
1663
1664        // Test simple import
1665        let import = analyzer
1666            .parse_use_statement("use std::collections::HashMap;")
1667            .unwrap();
1668        assert_eq!(import.path, "std::collections::HashMap");
1669        assert!(import.items.is_empty());
1670        assert!(import.alias.is_none());
1671    }
1672}