raz_core/
file_detection.rs

1//! File detection and project type analysis
2//!
3//! This module provides stateless file detection that can determine the execution
4//! context for any Rust file based solely on file path and content analysis.
5
6use crate::{Position, RazResult};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::{Path, PathBuf};
10
11#[cfg(feature = "tree-sitter-support")]
12use crate::tree_sitter_test_detector::TreeSitterTestDetector;
13
14/// Different types of Rust projects and files
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub enum RustProjectType {
17    /// Cargo workspace with multiple packages
18    CargoWorkspace {
19        root: PathBuf,
20        members: Vec<WorkspaceMember>,
21    },
22    /// Single cargo package
23    CargoPackage { root: PathBuf, package_name: String },
24    /// Cargo script with embedded manifest
25    CargoScript {
26        file_path: PathBuf,
27        manifest: Option<String>,
28    },
29    /// Single standalone Rust file
30    SingleFile {
31        file_path: PathBuf,
32        file_type: SingleFileType,
33    },
34}
35
36/// Types of single Rust files
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum SingleFileType {
39    /// Executable with main function
40    Executable,
41    /// Library with public API
42    Library,
43    /// Test file with test functions
44    Test,
45    /// Module file
46    Module,
47}
48
49/// Workspace member information
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct WorkspaceMember {
52    pub name: String,
53    pub path: PathBuf,
54    pub is_current: bool,
55}
56
57/// File execution context
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct FileExecutionContext {
60    pub project_type: RustProjectType,
61    pub file_role: FileRole,
62    pub entry_points: Vec<EntryPoint>,
63    pub capabilities: ExecutionCapabilities,
64    pub file_path: PathBuf,
65}
66
67impl FileExecutionContext {
68    /// Get the workspace root if this is a cargo project
69    pub fn get_workspace_root(&self) -> Option<&Path> {
70        match &self.project_type {
71            RustProjectType::CargoWorkspace { root, .. } => Some(root),
72            RustProjectType::CargoPackage { root, .. } => Some(root),
73            RustProjectType::CargoScript { .. } => None,
74            RustProjectType::SingleFile { .. } => None,
75        }
76    }
77}
78
79/// Role of the specific file in the project
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub enum FileRole {
82    /// Main binary entry point
83    MainBinary { binary_name: String },
84    /// Additional binary in bin/
85    AdditionalBinary { binary_name: String },
86    /// Library root (lib.rs)
87    LibraryRoot,
88    /// Frontend library in web framework
89    FrontendLibrary { framework: String },
90    /// Integration test
91    IntegrationTest { test_name: String },
92    /// Benchmark file
93    Benchmark { bench_name: String },
94    /// Example file
95    Example { example_name: String },
96    /// Build script
97    BuildScript,
98    /// Regular module
99    Module,
100    /// Standalone file
101    Standalone,
102}
103
104/// Entry points found in the file
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct EntryPoint {
107    pub name: String,
108    pub entry_type: EntryPointType,
109    pub line: u32,
110    pub column: u32,
111    /// Line range of the entry point (start, end)
112    pub line_range: (u32, u32),
113    /// Full path for tests (e.g., "tests::it_works" for tests in a module)
114    pub full_path: Option<String>,
115}
116
117/// Types of entry points
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub enum EntryPointType {
120    /// main() function
121    Main,
122    /// Test function
123    Test,
124    /// Test module (when cursor is in test module but not on specific test)
125    TestModule,
126    /// Benchmark function
127    Benchmark,
128    /// Example function (in examples)
129    Example,
130    /// Doc test
131    DocTest,
132}
133
134/// Execution capabilities for this file/project
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct ExecutionCapabilities {
137    pub can_run: bool,
138    pub can_test: bool,
139    pub can_bench: bool,
140    pub can_doc_test: bool,
141    pub requires_framework: Option<String>,
142}
143
144/// Stateless file detector
145pub struct FileDetector;
146
147impl FileDetector {
148    /// Detect execution context for any file path
149    pub fn detect_context(
150        file_path: &Path,
151        cursor: Option<Position>,
152    ) -> RazResult<FileExecutionContext> {
153        Self::detect_context_with_options(file_path, cursor, false)
154    }
155
156    /// Detect execution context with force standalone option
157    pub fn detect_context_with_options(
158        file_path: &Path,
159        cursor: Option<Position>,
160        force_standalone: bool,
161    ) -> RazResult<FileExecutionContext> {
162        let mut project_type = if force_standalone {
163            // Force standalone analysis, ignore any Cargo.toml
164            Self::analyze_single_file(file_path)?
165        } else {
166            Self::detect_project_type(file_path)?
167        };
168        let file_role = Self::detect_file_role(file_path, &project_type)?;
169
170        // If the file is standalone in a cargo project, treat it as a single file
171        if matches!(file_role, FileRole::Standalone)
172            && matches!(
173                project_type,
174                RustProjectType::CargoPackage { .. } | RustProjectType::CargoWorkspace { .. }
175            )
176        {
177            project_type = Self::analyze_single_file(file_path)?;
178        }
179
180        let entry_points = Self::detect_entry_points(file_path, cursor, &project_type)?;
181        let capabilities = Self::determine_capabilities(&project_type, &file_role, &entry_points);
182
183        Ok(FileExecutionContext {
184            project_type,
185            file_role,
186            entry_points,
187            capabilities,
188            file_path: file_path.to_path_buf(),
189        })
190    }
191
192    /// Detect the project type by crawling up the file system
193    fn detect_project_type(file_path: &Path) -> RazResult<RustProjectType> {
194        // Start from the file's directory
195        let mut current_dir = if file_path.is_file() {
196            file_path.parent().unwrap_or(file_path)
197        } else {
198            file_path
199        };
200
201        // Crawl up looking for Cargo.toml
202        loop {
203            let cargo_toml = current_dir.join("Cargo.toml");
204            if cargo_toml.exists() {
205                return Self::analyze_cargo_project(&cargo_toml, file_path);
206            }
207
208            match current_dir.parent() {
209                Some(parent) => current_dir = parent,
210                None => break,
211            }
212        }
213
214        // No Cargo.toml found, analyze as single file
215        Self::analyze_single_file(file_path)
216    }
217
218    /// Analyze a Cargo project
219    fn analyze_cargo_project(
220        cargo_toml_path: &Path,
221        file_path: &Path,
222    ) -> RazResult<RustProjectType> {
223        let content = fs::read_to_string(cargo_toml_path)?;
224        let root = cargo_toml_path.parent().unwrap().to_path_buf();
225
226        // Check if it's a workspace
227        if content.contains("[workspace]") {
228            let members = Self::parse_workspace_members(&content, &root)?;
229            Ok(RustProjectType::CargoWorkspace { root, members })
230        } else if content.contains("[package]") {
231            let package_name = Self::extract_package_name(&content)?;
232            Ok(RustProjectType::CargoPackage { root, package_name })
233        } else {
234            // Fallback to single file if Cargo.toml is malformed
235            Self::analyze_single_file(file_path)
236        }
237    }
238
239    /// Analyze a single file
240    fn analyze_single_file(file_path: &Path) -> RazResult<RustProjectType> {
241        let content = fs::read_to_string(file_path)?;
242
243        // Check for cargo script shebang
244        if Self::is_cargo_script(&content) {
245            let manifest = Self::extract_cargo_script_manifest(&content);
246            return Ok(RustProjectType::CargoScript {
247                file_path: file_path.to_path_buf(),
248                manifest,
249            });
250        }
251
252        // Determine single file type
253        let file_type = if content.contains("fn main(") {
254            SingleFileType::Executable
255        } else if content.contains("pub ") {
256            // If it has public API, it's a library (even if it also has tests)
257            SingleFileType::Library
258        } else if content.contains("#[test]") || content.contains("#[cfg(test)]") {
259            SingleFileType::Test
260        } else {
261            SingleFileType::Module
262        };
263
264        Ok(RustProjectType::SingleFile {
265            file_path: file_path.to_path_buf(),
266            file_type,
267        })
268    }
269
270    /// Detect the role of a specific file
271    fn detect_file_role(file_path: &Path, project_type: &RustProjectType) -> RazResult<FileRole> {
272        let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
273        let path_str = file_path.to_string_lossy();
274
275        // Handle different project types
276        match project_type {
277            RustProjectType::CargoWorkspace { .. } | RustProjectType::CargoPackage { .. } => {
278                if file_name == "build.rs" {
279                    Ok(FileRole::BuildScript)
280                } else if path_str.contains("/src/main.rs") {
281                    let binary_name = Self::extract_binary_name_from_path(file_path);
282                    Ok(FileRole::MainBinary { binary_name })
283                } else if path_str.contains("/src/bin/") {
284                    let binary_name = file_path
285                        .file_stem()
286                        .and_then(|s| s.to_str())
287                        .unwrap_or("unknown")
288                        .to_string();
289                    Ok(FileRole::AdditionalBinary { binary_name })
290                } else if file_name == "lib.rs" {
291                    // Check if this is a frontend library
292                    if Self::is_frontend_library(file_path) {
293                        let framework = Self::detect_web_framework(file_path)?;
294                        Ok(FileRole::FrontendLibrary { framework })
295                    } else {
296                        Ok(FileRole::LibraryRoot)
297                    }
298                } else if path_str.contains("/tests/") {
299                    let test_name = file_path
300                        .file_stem()
301                        .and_then(|s| s.to_str())
302                        .unwrap_or("unknown")
303                        .to_string();
304                    Ok(FileRole::IntegrationTest { test_name })
305                } else if path_str.contains("/benches/") {
306                    let bench_name = file_path
307                        .file_stem()
308                        .and_then(|s| s.to_str())
309                        .unwrap_or("unknown")
310                        .to_string();
311                    Ok(FileRole::Benchmark { bench_name })
312                } else if path_str.contains("/examples/") {
313                    let example_name = file_path
314                        .file_stem()
315                        .and_then(|s| s.to_str())
316                        .unwrap_or("unknown")
317                        .to_string();
318                    Ok(FileRole::Example { example_name })
319                } else if path_str.contains("/src/") && file_name.ends_with(".rs") {
320                    // Files in src/ subdirectories are modules
321                    Ok(FileRole::Module)
322                } else {
323                    // Files outside standard cargo directories should be treated as standalone
324                    // unless they're explicitly defined in Cargo.toml (which we don't check here)
325                    Ok(FileRole::Standalone)
326                }
327            }
328            RustProjectType::CargoScript { .. } => {
329                let binary_name = file_path
330                    .file_stem()
331                    .and_then(|s| s.to_str())
332                    .unwrap_or("script")
333                    .to_string();
334                Ok(FileRole::MainBinary { binary_name })
335            }
336            RustProjectType::SingleFile { file_type, .. } => {
337                // Check for special file names first
338                if file_name == "build.rs" {
339                    Ok(FileRole::BuildScript)
340                } else {
341                    match file_type {
342                        SingleFileType::Executable => {
343                            let binary_name = file_path
344                                .file_stem()
345                                .and_then(|s| s.to_str())
346                                .unwrap_or("main")
347                                .to_string();
348                            Ok(FileRole::MainBinary { binary_name })
349                        }
350                        _ => Ok(FileRole::Standalone),
351                    }
352                }
353            }
354        }
355    }
356
357    /// Detect entry points in the file
358    fn detect_entry_points(
359        file_path: &Path,
360        cursor: Option<Position>,
361        project_type: &RustProjectType,
362    ) -> RazResult<Vec<EntryPoint>> {
363        let content = fs::read_to_string(file_path)?;
364
365        // Use tree-sitter if available for accurate AST-based detection
366        #[cfg(feature = "tree-sitter-support")]
367        {
368            if let Ok(mut detector) = TreeSitterTestDetector::new() {
369                if let Ok(mut tree_sitter_entries) =
370                    detector.detect_test_entry_points(&content, cursor)
371                {
372                    // Build module path from file location for cargo projects
373                    let file_module_path =
374                        Self::build_module_path_from_file(file_path, project_type);
375
376                    // Update full paths for tests
377                    for entry in &mut tree_sitter_entries {
378                        if matches!(
379                            entry.entry_type,
380                            EntryPointType::Test | EntryPointType::TestModule
381                        ) {
382                            if let Some(existing_path) = &entry.full_path {
383                                // Combine file module path with test path
384                                let mut full_path_parts = file_module_path.clone();
385                                full_path_parts.push(existing_path.clone());
386                                entry.full_path = Some(full_path_parts.join("::"));
387                            } else if !file_module_path.is_empty() {
388                                // No existing path, use file module path + test name
389                                let mut full_path_parts = file_module_path.clone();
390                                full_path_parts.push(entry.name.clone());
391                                entry.full_path = Some(full_path_parts.join("::"));
392                            }
393                        }
394                    }
395
396                    // Also detect non-test entries using regex (main, benchmarks)
397                    tree_sitter_entries.extend(Self::detect_non_test_entries(&content)?);
398
399                    // Doc tests are now detected by tree-sitter in detect_test_entry_points above
400                    // No need for separate detection
401
402                    return Ok(tree_sitter_entries);
403                }
404            }
405            // Fall back to regex-based detection if tree-sitter fails
406        }
407
408        // Regex-based detection (fallback or when tree-sitter not available)
409        let mut entry_points = Vec::new();
410        let lines: Vec<&str> = content.lines().collect();
411
412        // Compile regex once outside the loop
413        let test_macro_regex = regex::Regex::new(r"#\[(\w+::)?test\]").unwrap();
414
415        // Build module path from file location for cargo projects
416        let file_module_path = Self::build_module_path_from_file(file_path, project_type);
417
418        // Track nested module hierarchy
419        let mut module_stack: Vec<String> = Vec::new();
420        let mut depth_stack: Vec<u32> = Vec::new();
421        let mut current_depth: u32 = 0;
422
423        // Track test module context
424        let mut in_test_module = false;
425        let mut test_module_depth = 0;
426
427        // Track doc test context
428        let mut in_doc_comment = false;
429        let mut in_doc_code_block = false;
430        let mut doc_test_start = None;
431        let mut doc_comment_start_line = None;
432
433        for (line_num, line) in lines.iter().enumerate() {
434            let trimmed = line.trim();
435
436            // Track brace depth and update module stack
437            let open_braces = trimmed.matches('{').count() as u32;
438            let close_braces = trimmed.matches('}').count() as u32;
439
440            // Handle closing braces first - pop modules that are ending
441            for _ in 0..close_braces {
442                current_depth = current_depth.saturating_sub(1);
443
444                // Pop module from stack if we're closing its scope
445                while let Some(&stack_depth) = depth_stack.last() {
446                    if current_depth < stack_depth {
447                        depth_stack.pop();
448                        module_stack.pop();
449                    } else {
450                        break;
451                    }
452                }
453
454                // Update test module state
455                if in_test_module && current_depth < test_module_depth {
456                    in_test_module = false;
457                }
458            }
459
460            // Handle opening braces
461            current_depth = current_depth.saturating_add(open_braces);
462
463            // Check for #[cfg(test)] attribute
464            if trimmed == "#[cfg(test)]" {
465                in_test_module = true;
466                test_module_depth = current_depth;
467            }
468
469            // Check for module declarations
470            if let Some(mod_start) = trimmed.find("mod ") {
471                let after_mod = &trimmed[mod_start + 4..];
472                if let Some(name_end) = after_mod.find([' ', '{', ';']) {
473                    let module_name = after_mod[..name_end].trim();
474
475                    // Only track modules that have a body (contain '{' or next line has '{')
476                    let has_body = trimmed.contains('{')
477                        || (line_num + 1 < lines.len()
478                            && lines[line_num + 1].trim().starts_with('{'));
479
480                    if has_body && !module_name.is_empty() {
481                        // Push this module onto the stack
482                        module_stack.push(module_name.to_string());
483                        depth_stack.push(current_depth);
484
485                        // Check if this is a test module
486                        if module_name.contains("test") && !in_test_module {
487                            in_test_module = true;
488                            test_module_depth = current_depth;
489                        }
490                    }
491                }
492            }
493
494            // Doc test detection
495            if trimmed.starts_with("///") || trimmed.starts_with("//!") {
496                if !in_doc_comment {
497                    doc_comment_start_line = Some(line_num);
498                }
499                in_doc_comment = true;
500                // Track code blocks in doc comments
501                if (trimmed == "/// ```"
502                    || trimmed == "/// ```rust"
503                    || trimmed == "//! ```"
504                    || trimmed == "//! ```rust")
505                    && !in_doc_code_block
506                {
507                    // Starting a code block
508                    in_doc_code_block = true;
509                    doc_test_start = Some(line_num as u32 + 1);
510                } else if trimmed == "/// ```" && in_doc_code_block {
511                    // Ending a code block
512                    in_doc_code_block = false;
513                }
514            } else if in_doc_comment && !trimmed.starts_with("//") {
515                // Doc comment ended, check if cursor is in doc test area
516                if let Some(cursor_pos) = cursor {
517                    if let Some(start) = doc_test_start {
518                        if cursor_pos.line + 1 >= start && cursor_pos.line < line_num as u32 + 1 {
519                            // Check if current line is a function/struct/impl declaration
520                            let mut item_name = None;
521                            if trimmed.starts_with("pub fn") || trimmed.starts_with("fn") {
522                                item_name = Self::extract_function_name(trimmed);
523                            } else if trimmed.starts_with("pub struct")
524                                || trimmed.starts_with("struct")
525                            {
526                                item_name = Self::extract_struct_name(trimmed);
527                            } else if trimmed.starts_with("impl") {
528                                item_name = Self::extract_impl_name(trimmed);
529                            } else {
530                                // Look ahead for the function/struct/impl this doc comment belongs to
531                                for i in 1..5 {
532                                    if line_num + i < lines.len() {
533                                        let ahead_line = lines[line_num + i].trim();
534                                        if ahead_line.starts_with("pub fn")
535                                            || ahead_line.starts_with("fn")
536                                        {
537                                            item_name = Self::extract_function_name(ahead_line);
538                                            break;
539                                        } else if ahead_line.starts_with("pub struct")
540                                            || ahead_line.starts_with("struct")
541                                        {
542                                            item_name = Self::extract_struct_name(ahead_line);
543                                            break;
544                                        } else if ahead_line.starts_with("impl") {
545                                            item_name = Self::extract_impl_name(ahead_line);
546                                            break;
547                                        }
548                                    }
549                                }
550                            }
551
552                            if let Some(name) = item_name {
553                                entry_points.push(EntryPoint {
554                                    name: name.clone(),
555                                    entry_type: EntryPointType::DocTest,
556                                    line: start,
557                                    column: 0,
558                                    line_range: (
559                                        doc_comment_start_line.unwrap_or(0) as u32 + 1,
560                                        (line_num as u32).saturating_sub(1), // Exclude the function declaration line
561                                    ),
562                                    full_path: Some(name),
563                                });
564                            }
565                        }
566                    }
567                }
568
569                in_doc_comment = false;
570                in_doc_code_block = false;
571                doc_test_start = None;
572                doc_comment_start_line = None;
573            }
574
575            // Main function
576            if trimmed.contains("fn main(") {
577                entry_points.push(EntryPoint {
578                    name: "main".to_string(),
579                    entry_type: EntryPointType::Main,
580                    line: line_num as u32 + 1,
581                    column: line.find("fn main(").unwrap_or(0) as u32,
582                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
583                    full_path: None,
584                });
585            }
586
587            // Helper function to build current module path
588            let build_module_path = |fn_name: &str| -> Option<String> {
589                let mut path_parts = file_module_path.clone();
590                path_parts.extend(module_stack.clone());
591                path_parts.push(fn_name.to_string());
592                if path_parts.is_empty() {
593                    None
594                } else {
595                    Some(path_parts.join("::"))
596                }
597            };
598
599            // Test functions with regex pattern matching
600            if test_macro_regex.is_match(trimmed) {
601                // Look for the function on the next line(s)
602                for i in 1..=3 {
603                    if line_num + i < lines.len() {
604                        let next_line = lines[line_num + i];
605                        if let Some(fn_name) = Self::extract_function_name(next_line) {
606                            let full_path = build_module_path(&fn_name);
607                            entry_points.push(EntryPoint {
608                                name: fn_name.clone(),
609                                entry_type: EntryPointType::Test,
610                                line: line_num as u32 + i as u32 + 1,
611                                column: 0,
612                                line_range: (line_num as u32 + 1, line_num as u32 + 1),
613                                full_path,
614                            });
615                            break;
616                        }
617                    }
618                }
619            } else if (trimmed.starts_with("fn test_") || trimmed.contains("fn test_"))
620                && (in_test_module || trimmed.contains("#["))
621            {
622                if let Some(fn_name) = Self::extract_function_name(trimmed) {
623                    let column = line.find(&format!("fn {fn_name}")).unwrap_or(0) as u32;
624                    let full_path = build_module_path(&fn_name);
625                    entry_points.push(EntryPoint {
626                        name: fn_name.clone(),
627                        entry_type: EntryPointType::Test,
628                        line: line_num as u32 + 1,
629                        column,
630                        line_range: (line_num as u32 + 1, line_num as u32 + 1),
631                        full_path,
632                    });
633                }
634            }
635
636            // Benchmark functions
637            if trimmed.starts_with("#[bench]") {
638                if let Some(next_line) = lines.get(line_num + 1) {
639                    if let Some(fn_name) = Self::extract_function_name(next_line) {
640                        entry_points.push(EntryPoint {
641                            name: fn_name,
642                            entry_type: EntryPointType::Benchmark,
643                            line: line_num as u32 + 2,
644                            column: 0,
645                            line_range: (line_num as u32 + 1, line_num as u32 + 1),
646                            full_path: None,
647                        });
648                    }
649                }
650            }
651
652            // criterion benchmarks
653            if trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!") {
654                entry_points.push(EntryPoint {
655                    name: "criterion_bench".to_string(),
656                    entry_type: EntryPointType::Benchmark,
657                    line: line_num as u32 + 1,
658                    column: 0,
659                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
660                    full_path: None,
661                });
662            }
663        }
664
665        // Final check: if we're still in a doc comment at the end of the loop and cursor is in it
666        // This handles the case where the file ends with a doc comment
667        if in_doc_comment && cursor.is_some() {
668            let cursor_pos = cursor.unwrap();
669            if let Some(start) = doc_test_start {
670                if cursor_pos.line + 1 >= start {
671                    // The doc comment goes to the end of the file, so we can't find a function after it
672                    // Use a generic name for now
673                    entry_points.push(EntryPoint {
674                        name: "doc_test".to_string(),
675                        entry_type: EntryPointType::DocTest,
676                        line: start,
677                        column: 0,
678                        line_range: (
679                            doc_comment_start_line.unwrap_or(0) as u32 + 1,
680                            lines.len() as u32,
681                        ),
682                        full_path: None,
683                    });
684                }
685            }
686        }
687
688        // Add test module entry if cursor is in a test module but not on a specific test
689        if let Some(cursor_pos) = cursor {
690            if in_test_module && !module_stack.is_empty() {
691                // Check if cursor is in test module but not directly on a test function
692                let cursor_line = cursor_pos.line + 1;
693                let on_specific_test = entry_points
694                    .iter()
695                    .any(|ep| ep.entry_type == EntryPointType::Test && ep.line == cursor_line);
696
697                if !on_specific_test {
698                    let mut path_parts = file_module_path.clone();
699                    path_parts.extend(module_stack.clone());
700                    let current_module_path = path_parts.join("::");
701                    entry_points.push(EntryPoint {
702                        name: format!(
703                            "{}_module",
704                            module_stack.last().unwrap_or(&"tests".to_string())
705                        ),
706                        entry_type: EntryPointType::TestModule,
707                        line: cursor_line,
708                        column: 0,
709                        line_range: (cursor_line, cursor_line),
710                        full_path: Some(current_module_path),
711                    });
712                }
713            }
714        }
715
716        Ok(entry_points)
717    }
718
719    /// Determine execution capabilities
720    fn determine_capabilities(
721        project_type: &RustProjectType,
722        file_role: &FileRole,
723        entry_points: &[EntryPoint],
724    ) -> ExecutionCapabilities {
725        let has_main = entry_points
726            .iter()
727            .any(|ep| ep.entry_type == EntryPointType::Main);
728        let has_tests = entry_points
729            .iter()
730            .any(|ep| ep.entry_type == EntryPointType::Test);
731        let has_benches = entry_points
732            .iter()
733            .any(|ep| ep.entry_type == EntryPointType::Benchmark);
734
735        match file_role {
736            FileRole::MainBinary { .. } | FileRole::AdditionalBinary { .. } => {
737                ExecutionCapabilities {
738                    can_run: has_main,
739                    can_test: has_tests,
740                    can_bench: has_benches,
741                    can_doc_test: false,
742                    requires_framework: None,
743                }
744            }
745            FileRole::FrontendLibrary { framework } => ExecutionCapabilities {
746                can_run: true, // Framework-specific run
747                can_test: has_tests,
748                can_bench: has_benches,
749                can_doc_test: true,
750                requires_framework: Some(framework.clone()),
751            },
752            FileRole::LibraryRoot => ExecutionCapabilities {
753                can_run: false,
754                can_test: has_tests,
755                can_bench: has_benches,
756                can_doc_test: true,
757                requires_framework: None,
758            },
759            FileRole::IntegrationTest { .. } => ExecutionCapabilities {
760                can_run: false,
761                can_test: true,
762                can_bench: false,
763                can_doc_test: false,
764                requires_framework: None,
765            },
766            FileRole::Benchmark { .. } => ExecutionCapabilities {
767                can_run: false,
768                can_test: false,
769                can_bench: true,
770                can_doc_test: false,
771                requires_framework: None,
772            },
773            FileRole::Example { .. } => ExecutionCapabilities {
774                can_run: has_main,
775                can_test: has_tests,
776                can_bench: has_benches,
777                can_doc_test: false,
778                requires_framework: None,
779            },
780            FileRole::BuildScript => ExecutionCapabilities {
781                can_run: has_main,
782                can_test: false,
783                can_bench: false,
784                can_doc_test: false,
785                requires_framework: None,
786            },
787            FileRole::Standalone => match project_type {
788                RustProjectType::SingleFile { file_type, .. } => match file_type {
789                    SingleFileType::Executable => ExecutionCapabilities {
790                        can_run: has_main,
791                        can_test: has_tests,
792                        can_bench: has_benches,
793                        can_doc_test: false,
794                        requires_framework: None,
795                    },
796                    SingleFileType::Library => ExecutionCapabilities {
797                        can_run: false,
798                        can_test: has_tests,
799                        can_bench: has_benches,
800                        can_doc_test: true,
801                        requires_framework: None,
802                    },
803                    SingleFileType::Test => ExecutionCapabilities {
804                        can_run: false,
805                        can_test: true,
806                        can_bench: false,
807                        can_doc_test: false,
808                        requires_framework: None,
809                    },
810                    SingleFileType::Module => ExecutionCapabilities {
811                        can_run: false,
812                        can_test: has_tests,
813                        can_bench: false,
814                        can_doc_test: false,
815                        requires_framework: None,
816                    },
817                },
818                RustProjectType::CargoScript { .. } => ExecutionCapabilities {
819                    can_run: has_main,
820                    can_test: has_tests,
821                    can_bench: has_benches,
822                    can_doc_test: false,
823                    requires_framework: None,
824                },
825                _ => {
826                    // For standalone files in cargo projects, determine capabilities based on content
827                    ExecutionCapabilities {
828                        can_run: has_main,
829                        can_test: has_tests,
830                        can_bench: has_benches,
831                        can_doc_test: false,
832                        requires_framework: None,
833                    }
834                }
835            },
836            FileRole::Module => ExecutionCapabilities {
837                can_run: false,
838                can_test: has_tests,
839                can_bench: has_benches,
840                can_doc_test: false,
841                requires_framework: None,
842            },
843        }
844    }
845
846    // Helper methods
847
848    fn is_cargo_script(content: &str) -> bool {
849        content.starts_with("#!/usr/bin/env -S cargo")
850            || content.starts_with("#!/usr/bin/env cargo")
851            || content.starts_with("#!/usr/bin/env cargo-eval")
852            || content.starts_with("#!/usr/bin/env run-cargo-script")
853    }
854
855    fn extract_cargo_script_manifest(content: &str) -> Option<String> {
856        // Look for embedded manifest
857        if let Some(start) = content.find("//! ```cargo") {
858            let search_start = start + 13; // Length of "//! ```cargo"
859            if let Some(relative_end) = content[search_start..].find("//! ```") {
860                let end = search_start + relative_end;
861                return Some(content[search_start..end].trim().to_string());
862            }
863        }
864        None
865    }
866
867    fn parse_workspace_members(content: &str, root: &Path) -> RazResult<Vec<WorkspaceMember>> {
868        let mut members = Vec::new();
869
870        // Simple parsing - in a real implementation, use a TOML parser
871        if let Some(members_section) = content.find("members = [") {
872            let section = &content[members_section..];
873            if let Some(end) = section.find(']') {
874                let members_str = &section[11..end];
875                for member in members_str.split(',') {
876                    let member_name = member.trim().trim_matches('"').trim_matches('\'');
877                    if !member_name.is_empty() {
878                        members.push(WorkspaceMember {
879                            name: member_name.to_string(),
880                            path: root.join(member_name),
881                            is_current: false, // Will be determined later
882                        });
883                    }
884                }
885            }
886        }
887
888        Ok(members)
889    }
890
891    fn extract_package_name(content: &str) -> RazResult<String> {
892        if let Some(name_start) = content.find("name = \"") {
893            let name_section = &content[name_start + 8..];
894            if let Some(name_end) = name_section.find('"') {
895                return Ok(name_section[..name_end].to_string());
896            }
897        }
898        Ok("unknown".to_string())
899    }
900
901    fn extract_binary_name_from_path(file_path: &Path) -> String {
902        // For src/main.rs, try to determine package name from Cargo.toml
903        if let Some(parent) = file_path.parent() {
904            if let Some(cargo_dir) = parent.parent() {
905                let cargo_toml = cargo_dir.join("Cargo.toml");
906                if cargo_toml.exists() {
907                    if let Ok(content) = fs::read_to_string(&cargo_toml) {
908                        if let Ok(name) = Self::extract_package_name(&content) {
909                            return name;
910                        }
911                    }
912                }
913            }
914        }
915        "main".to_string()
916    }
917
918    fn is_frontend_library(file_path: &Path) -> bool {
919        let path_str = file_path.to_string_lossy();
920        let file_name = file_path.file_name().unwrap_or_default().to_string_lossy();
921
922        file_name == "lib.rs"
923            && (path_str.contains("/frontend/")
924                || path_str.contains("/app/")
925                || path_str.contains("/client/")
926                || path_str.contains("/web/"))
927    }
928
929    fn detect_web_framework(file_path: &Path) -> RazResult<String> {
930        // Walk up to find Cargo.toml and check dependencies
931        let mut current = file_path;
932        while let Some(parent) = current.parent() {
933            let cargo_toml = parent.join("Cargo.toml");
934            if cargo_toml.exists() {
935                if let Ok(content) = fs::read_to_string(&cargo_toml) {
936                    if content.contains("leptos") {
937                        return Ok("leptos".to_string());
938                    } else if content.contains("dioxus") {
939                        return Ok("dioxus".to_string());
940                    } else if content.contains("yew") {
941                        return Ok("yew".to_string());
942                    }
943                }
944            }
945            current = parent;
946        }
947        Ok("unknown".to_string())
948    }
949
950    fn detect_non_test_entries(content: &str) -> RazResult<Vec<EntryPoint>> {
951        let mut entry_points = Vec::new();
952        let lines: Vec<&str> = content.lines().collect();
953        let mut in_doc_comment = false;
954
955        for (line_num, line) in lines.iter().enumerate() {
956            let trimmed = line.trim();
957
958            // Track doc comments to avoid false positives
959            if trimmed.starts_with("///") || trimmed.starts_with("//!") {
960                in_doc_comment = true;
961            } else if in_doc_comment && !trimmed.starts_with("//") {
962                in_doc_comment = false;
963            }
964
965            // Main function - only detect if not in doc comment
966            if !in_doc_comment && trimmed.contains("fn main(") {
967                entry_points.push(EntryPoint {
968                    name: "main".to_string(),
969                    entry_type: EntryPointType::Main,
970                    line: line_num as u32 + 1,
971                    column: line.find("fn main(").unwrap_or(0) as u32,
972                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
973                    full_path: None,
974                });
975            }
976
977            // Benchmark functions - only detect if not in doc comment
978            if !in_doc_comment && trimmed.starts_with("#[bench]") {
979                if let Some(next_line) = lines.get(line_num + 1) {
980                    if let Some(fn_name) = Self::extract_function_name(next_line) {
981                        entry_points.push(EntryPoint {
982                            name: fn_name,
983                            entry_type: EntryPointType::Benchmark,
984                            line: line_num as u32 + 2,
985                            column: 0,
986                            line_range: (line_num as u32 + 1, line_num as u32 + 1),
987                            full_path: None,
988                        });
989                    }
990                }
991            }
992
993            // criterion benchmarks - only detect if not in doc comment
994            if !in_doc_comment
995                && (trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!"))
996            {
997                entry_points.push(EntryPoint {
998                    name: "criterion_bench".to_string(),
999                    entry_type: EntryPointType::Benchmark,
1000                    line: line_num as u32 + 1,
1001                    column: 0,
1002                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
1003                    full_path: None,
1004                });
1005            }
1006        }
1007
1008        Ok(entry_points)
1009    }
1010
1011    fn extract_function_name(line: &str) -> Option<String> {
1012        let trimmed = line.trim();
1013        if let Some(fn_start) = trimmed.find("fn ") {
1014            let fn_part = &trimmed[fn_start + 3..];
1015            if let Some(paren_pos) = fn_part.find('(') {
1016                let fn_name = fn_part[..paren_pos].trim();
1017                if !fn_name.is_empty() {
1018                    return Some(fn_name.to_string());
1019                }
1020            }
1021        }
1022        None
1023    }
1024
1025    fn extract_struct_name(line: &str) -> Option<String> {
1026        let trimmed = line.trim();
1027        let struct_start = if trimmed.starts_with("pub struct ") {
1028            11 // Length of "pub struct "
1029        } else if trimmed.starts_with("struct ") {
1030            7 // Length of "struct "
1031        } else {
1032            return None;
1033        };
1034
1035        let after_struct = &trimmed[struct_start..];
1036        // Find the end of the struct name (space, <, {, or ;)
1037        let name_end = after_struct
1038            .find([' ', '<', '{', ';'])
1039            .unwrap_or(after_struct.len());
1040        let struct_name = after_struct[..name_end].trim();
1041
1042        if !struct_name.is_empty() {
1043            Some(struct_name.to_string())
1044        } else {
1045            None
1046        }
1047    }
1048
1049    fn extract_impl_name(line: &str) -> Option<String> {
1050        let trimmed = line.trim();
1051        if !trimmed.starts_with("impl") {
1052            return None;
1053        }
1054
1055        // Handle "impl Trait for Type" or "impl Type"
1056        if let Some(for_pos) = trimmed.find(" for ") {
1057            // Extract the type after "for"
1058            let after_for = &trimmed[for_pos + 5..];
1059            let name_end = after_for.find([' ', '{', '<']).unwrap_or(after_for.len());
1060            let type_name = after_for[..name_end].trim();
1061            if !type_name.is_empty() {
1062                return Some(type_name.to_string());
1063            }
1064        } else {
1065            // Handle "impl Type" or "impl<T> Type"
1066            let after_impl = if trimmed.starts_with("impl<") {
1067                // Skip generic parameters
1068                if let Some(gt_pos) = trimmed.find('>') {
1069                    &trimmed[gt_pos + 1..].trim()
1070                } else {
1071                    return None;
1072                }
1073            } else {
1074                &trimmed[4..].trim() // Skip "impl"
1075            };
1076
1077            let name_end = after_impl.find([' ', '{', '<']).unwrap_or(after_impl.len());
1078            let type_name = after_impl[..name_end].trim();
1079            if !type_name.is_empty() {
1080                return Some(type_name.to_string());
1081            }
1082        }
1083        None
1084    }
1085
1086    /// Build module path from file path relative to src/
1087    fn build_module_path_from_file(
1088        file_path: &Path,
1089        project_type: &RustProjectType,
1090    ) -> Vec<String> {
1091        let mut module_path = Vec::new();
1092
1093        // Only build module paths for cargo projects
1094        let src_dir = match project_type {
1095            RustProjectType::CargoWorkspace { root, .. } => root.join("src"),
1096            RustProjectType::CargoPackage { root, .. } => root.join("src"),
1097            _ => return module_path,
1098        };
1099
1100        // Get the relative path from src/
1101        if let Ok(relative_path) = file_path.strip_prefix(&src_dir) {
1102            // Convert path components to module names
1103            for component in relative_path.components() {
1104                if let std::path::Component::Normal(os_str) = component {
1105                    if let Some(name) = os_str.to_str() {
1106                        // Remove .rs extension and handle special names
1107                        let module_name = if name == "lib.rs" || name == "main.rs" {
1108                            continue; // lib.rs and main.rs are root modules
1109                        } else if name == "mod.rs" {
1110                            // mod.rs represents the parent directory name
1111                            if let Some(parent) = relative_path.parent() {
1112                                if let Some(parent_name) = parent.file_name() {
1113                                    parent_name.to_str().unwrap_or("").to_string()
1114                                } else {
1115                                    continue;
1116                                }
1117                            } else {
1118                                continue;
1119                            }
1120                        } else if name.ends_with(".rs") {
1121                            name.trim_end_matches(".rs").to_string()
1122                        } else {
1123                            // Directory name
1124                            name.to_string()
1125                        };
1126
1127                        if !module_name.is_empty() {
1128                            module_path.push(module_name);
1129                        }
1130                    }
1131                }
1132            }
1133        }
1134
1135        module_path
1136    }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141    use super::*;
1142    use tempfile::TempDir;
1143
1144    #[test]
1145    fn test_cargo_script_detection() {
1146        let content = "#!/usr/bin/env -S cargo +nightly -Zscript\n\nfn main() {}";
1147        assert!(FileDetector::is_cargo_script(content));
1148    }
1149
1150    #[test]
1151    fn test_function_name_extraction() {
1152        assert_eq!(
1153            FileDetector::extract_function_name("fn test_something() {"),
1154            Some("test_something".to_string())
1155        );
1156        assert_eq!(
1157            FileDetector::extract_function_name("    fn main() {"),
1158            Some("main".to_string())
1159        );
1160    }
1161
1162    #[test]
1163    fn test_frontend_library_detection() {
1164        let temp_dir = TempDir::new().unwrap();
1165        let frontend_lib = temp_dir.path().join("frontend").join("src").join("lib.rs");
1166        assert!(FileDetector::is_frontend_library(&frontend_lib));
1167
1168        let regular_lib = temp_dir.path().join("src").join("lib.rs");
1169        assert!(!FileDetector::is_frontend_library(&regular_lib));
1170    }
1171
1172    #[test]
1173    fn test_nested_module_path_resolution() -> RazResult<()> {
1174        let temp_dir = TempDir::new()?;
1175        let test_file = temp_dir.path().join("nested_test.rs");
1176
1177        let content = r#"
1178#[cfg(test)]
1179mod tests {
1180    use super::*;
1181
1182    #[test]
1183    fn test_top_level() {
1184        assert_eq!(2 + 2, 4);
1185    }
1186
1187    mod integration {
1188        use super::*;
1189
1190        #[test] 
1191        fn test_integration_basic() {
1192            assert!(true);
1193        }
1194
1195        mod database {
1196            use super::*;
1197
1198            #[test]
1199            fn test_db_connection() {
1200                assert!(true);
1201            }
1202
1203            mod transactions {
1204                use super::*;
1205
1206                #[test]
1207                fn test_transaction_rollback() {
1208                    assert!(true);
1209                }
1210            }
1211        }
1212    }
1213}
1214"#;
1215
1216        fs::write(&test_file, content)?;
1217
1218        // Test detecting nested test at deeply nested level (line 28 - test_transaction_rollback)
1219        let context = FileDetector::detect_context(
1220            &test_file,
1221            Some(Position {
1222                line: 27,
1223                column: 1,
1224            }),
1225        )?;
1226
1227        // Find the deeply nested test
1228        let nested_test = context.entry_points.iter().find(|ep| {
1229            ep.name == "test_transaction_rollback" && ep.entry_type == EntryPointType::Test
1230        });
1231
1232        assert!(
1233            nested_test.is_some(),
1234            "Should find the deeply nested test function"
1235        );
1236
1237        let test_entry = nested_test.unwrap();
1238        assert_eq!(
1239            test_entry.full_path.as_ref().unwrap(),
1240            "tests::integration::database::transactions::test_transaction_rollback",
1241            "Should have correct full module path for deeply nested test"
1242        );
1243
1244        // Test detecting test at intermediate level (line 20 - test_db_connection)
1245        let context2 = FileDetector::detect_context(
1246            &test_file,
1247            Some(Position {
1248                line: 19,
1249                column: 1,
1250            }),
1251        )?;
1252
1253        let mid_level_test = context2
1254            .entry_points
1255            .iter()
1256            .find(|ep| ep.name == "test_db_connection" && ep.entry_type == EntryPointType::Test);
1257
1258        assert!(
1259            mid_level_test.is_some(),
1260            "Should find the mid-level nested test"
1261        );
1262
1263        let test_entry2 = mid_level_test.unwrap();
1264        assert_eq!(
1265            test_entry2.full_path.as_ref().unwrap(),
1266            "tests::integration::database::test_db_connection",
1267            "Should have correct full module path for mid-level nested test"
1268        );
1269
1270        // Test detecting top-level test
1271        let top_level_test = context
1272            .entry_points
1273            .iter()
1274            .find(|ep| ep.name == "test_top_level" && ep.entry_type == EntryPointType::Test);
1275
1276        assert!(top_level_test.is_some(), "Should find the top-level test");
1277
1278        let test_entry3 = top_level_test.unwrap();
1279        assert_eq!(
1280            test_entry3.full_path.as_ref().unwrap(),
1281            "tests::test_top_level",
1282            "Should have correct module path for top-level test"
1283        );
1284
1285        Ok(())
1286    }
1287
1288    #[test]
1289    fn test_module_stack_handling_with_complex_nesting() -> RazResult<()> {
1290        let temp_dir = TempDir::new()?;
1291        let test_file = temp_dir.path().join("complex_nested.rs");
1292
1293        let content = r#"
1294mod outer {
1295    mod middle {
1296        #[cfg(test)]
1297        mod tests {
1298            #[test]
1299            fn test_deeply_nested() {
1300                assert!(true);
1301            }
1302            
1303            mod sub_tests {
1304                #[test]
1305                fn test_sub_nested() {
1306                    assert!(true);
1307                }
1308            }
1309        }
1310        
1311        mod other {
1312            fn regular_function() {}
1313        }
1314    }
1315}
1316
1317#[cfg(test)]
1318mod main_tests {
1319    #[test]
1320    fn test_main_level() {
1321        assert!(true);
1322    }
1323}
1324"#;
1325
1326        fs::write(&test_file, content)?;
1327
1328        let context = FileDetector::detect_context(&test_file, None)?;
1329
1330        // Check that we correctly detect tests in different nested locations
1331        let deeply_nested = context
1332            .entry_points
1333            .iter()
1334            .find(|ep| ep.name == "test_deeply_nested");
1335
1336        assert!(deeply_nested.is_some(), "Should find deeply nested test");
1337        assert_eq!(
1338            deeply_nested.unwrap().full_path.as_ref().unwrap(),
1339            "outer::middle::tests::test_deeply_nested",
1340            "Should correctly build path through multiple modules"
1341        );
1342
1343        let sub_nested = context
1344            .entry_points
1345            .iter()
1346            .find(|ep| ep.name == "test_sub_nested");
1347
1348        assert!(sub_nested.is_some(), "Should find sub-nested test");
1349        assert_eq!(
1350            sub_nested.unwrap().full_path.as_ref().unwrap(),
1351            "outer::middle::tests::sub_tests::test_sub_nested",
1352            "Should correctly build path through all nested modules"
1353        );
1354
1355        let main_test = context
1356            .entry_points
1357            .iter()
1358            .find(|ep| ep.name == "test_main_level");
1359
1360        assert!(main_test.is_some(), "Should find main level test");
1361        assert_eq!(
1362            main_test.unwrap().full_path.as_ref().unwrap(),
1363            "main_tests::test_main_level",
1364            "Should correctly build path for main level test"
1365        );
1366
1367        Ok(())
1368    }
1369}