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) = detector.detect_entry_points(&content, cursor)
370                {
371                    // Build module path from file location for cargo projects
372                    let file_module_path =
373                        Self::build_module_path_from_file(file_path, project_type);
374
375                    // Update full paths for tests
376                    for entry in &mut tree_sitter_entries {
377                        if matches!(
378                            entry.entry_type,
379                            EntryPointType::Test | EntryPointType::TestModule
380                        ) {
381                            if let Some(existing_path) = &entry.full_path {
382                                // Combine file module path with test path
383                                let mut full_path_parts = file_module_path.clone();
384                                full_path_parts.push(existing_path.clone());
385                                entry.full_path = Some(full_path_parts.join("::"));
386                            } else if !file_module_path.is_empty() {
387                                // No existing path, use file module path + test name
388                                let mut full_path_parts = file_module_path.clone();
389                                full_path_parts.push(entry.name.clone());
390                                entry.full_path = Some(full_path_parts.join("::"));
391                            }
392                        }
393                    }
394
395                    // Also detect non-test entries using regex (main, benchmarks)
396                    tree_sitter_entries.extend(Self::detect_non_test_entries(&content)?);
397
398                    // Doc tests are now detected by tree-sitter in detect_test_entry_points above
399                    // No need for separate detection
400
401                    return Ok(tree_sitter_entries);
402                }
403            }
404            // Fall back to regex-based detection if tree-sitter fails
405        }
406
407        // Regex-based detection (fallback or when tree-sitter not available)
408        let mut entry_points = Vec::new();
409        let lines: Vec<&str> = content.lines().collect();
410
411        // Compile regex once outside the loop
412        let test_macro_regex = regex::Regex::new(r"#\[(\w+::)?test\]").unwrap();
413
414        // Build module path from file location for cargo projects
415        let file_module_path = Self::build_module_path_from_file(file_path, project_type);
416
417        // Track nested module hierarchy
418        let mut module_stack: Vec<String> = Vec::new();
419        let mut depth_stack: Vec<u32> = Vec::new();
420        let mut current_depth: u32 = 0;
421
422        // Track test module context
423        let mut in_test_module = false;
424        let mut test_module_depth = 0;
425
426        // Track doc test context
427        let mut in_doc_comment = false;
428        let mut in_doc_code_block = false;
429        let mut doc_test_start = None;
430        let mut doc_comment_start_line = None;
431
432        for (line_num, line) in lines.iter().enumerate() {
433            let trimmed = line.trim();
434
435            // Track brace depth and update module stack
436            let open_braces = trimmed.matches('{').count() as u32;
437            let close_braces = trimmed.matches('}').count() as u32;
438
439            // Handle closing braces first - pop modules that are ending
440            for _ in 0..close_braces {
441                current_depth = current_depth.saturating_sub(1);
442
443                // Pop module from stack if we're closing its scope
444                while let Some(&stack_depth) = depth_stack.last() {
445                    if current_depth < stack_depth {
446                        depth_stack.pop();
447                        module_stack.pop();
448                    } else {
449                        break;
450                    }
451                }
452
453                // Update test module state
454                if in_test_module && current_depth < test_module_depth {
455                    in_test_module = false;
456                }
457            }
458
459            // Handle opening braces
460            current_depth = current_depth.saturating_add(open_braces);
461
462            // Check for #[cfg(test)] attribute
463            if trimmed == "#[cfg(test)]" {
464                in_test_module = true;
465                test_module_depth = current_depth;
466            }
467
468            // Check for module declarations
469            if let Some(mod_start) = trimmed.find("mod ") {
470                let after_mod = &trimmed[mod_start + 4..];
471                if let Some(name_end) = after_mod.find([' ', '{', ';']) {
472                    let module_name = after_mod[..name_end].trim();
473
474                    // Only track modules that have a body (contain '{' or next line has '{')
475                    let has_body = trimmed.contains('{')
476                        || (line_num + 1 < lines.len()
477                            && lines[line_num + 1].trim().starts_with('{'));
478
479                    if has_body && !module_name.is_empty() {
480                        // Push this module onto the stack
481                        module_stack.push(module_name.to_string());
482                        depth_stack.push(current_depth);
483
484                        // Check if this is a test module
485                        if module_name.contains("test") && !in_test_module {
486                            in_test_module = true;
487                            test_module_depth = current_depth;
488                        }
489                    }
490                }
491            }
492
493            // Doc test detection
494            if trimmed.starts_with("///") || trimmed.starts_with("//!") {
495                if !in_doc_comment {
496                    doc_comment_start_line = Some(line_num);
497                }
498                in_doc_comment = true;
499                // Track code blocks in doc comments
500                if (trimmed == "/// ```"
501                    || trimmed == "/// ```rust"
502                    || trimmed == "//! ```"
503                    || trimmed == "//! ```rust")
504                    && !in_doc_code_block
505                {
506                    // Starting a code block
507                    in_doc_code_block = true;
508                    doc_test_start = Some(line_num as u32 + 1);
509                } else if trimmed == "/// ```" && in_doc_code_block {
510                    // Ending a code block
511                    in_doc_code_block = false;
512                }
513            } else if in_doc_comment && !trimmed.starts_with("//") {
514                // Doc comment ended, check if cursor is in doc test area
515                if let Some(cursor_pos) = cursor {
516                    if let Some(start) = doc_test_start {
517                        if cursor_pos.line + 1 >= start && cursor_pos.line < line_num as u32 + 1 {
518                            // Check if current line is a function/struct/impl declaration
519                            let mut item_name = None;
520                            if trimmed.starts_with("pub fn") || trimmed.starts_with("fn") {
521                                item_name = Self::extract_function_name(trimmed);
522                            } else if trimmed.starts_with("pub struct")
523                                || trimmed.starts_with("struct")
524                            {
525                                item_name = Self::extract_struct_name(trimmed);
526                            } else if trimmed.starts_with("impl") {
527                                item_name = Self::extract_impl_name(trimmed);
528                            } else {
529                                // Look ahead for the function/struct/impl this doc comment belongs to
530                                for i in 1..5 {
531                                    if line_num + i < lines.len() {
532                                        let ahead_line = lines[line_num + i].trim();
533                                        if ahead_line.starts_with("pub fn")
534                                            || ahead_line.starts_with("fn")
535                                        {
536                                            item_name = Self::extract_function_name(ahead_line);
537                                            break;
538                                        } else if ahead_line.starts_with("pub struct")
539                                            || ahead_line.starts_with("struct")
540                                        {
541                                            item_name = Self::extract_struct_name(ahead_line);
542                                            break;
543                                        } else if ahead_line.starts_with("impl") {
544                                            item_name = Self::extract_impl_name(ahead_line);
545                                            break;
546                                        }
547                                    }
548                                }
549                            }
550
551                            if let Some(name) = item_name {
552                                entry_points.push(EntryPoint {
553                                    name: name.clone(),
554                                    entry_type: EntryPointType::DocTest,
555                                    line: start,
556                                    column: 0,
557                                    line_range: (
558                                        doc_comment_start_line.unwrap_or(0) as u32 + 1,
559                                        (line_num as u32).saturating_sub(1), // Exclude the function declaration line
560                                    ),
561                                    full_path: Some(name),
562                                });
563                            }
564                        }
565                    }
566                }
567
568                in_doc_comment = false;
569                in_doc_code_block = false;
570                doc_test_start = None;
571                doc_comment_start_line = None;
572            }
573
574            // Main function
575            if trimmed.contains("fn main(") {
576                entry_points.push(EntryPoint {
577                    name: "main".to_string(),
578                    entry_type: EntryPointType::Main,
579                    line: line_num as u32 + 1,
580                    column: line.find("fn main(").unwrap_or(0) as u32,
581                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
582                    full_path: None,
583                });
584            }
585
586            // Helper function to build current module path
587            let build_module_path = |fn_name: &str| -> Option<String> {
588                let mut path_parts = file_module_path.clone();
589                path_parts.extend(module_stack.clone());
590                path_parts.push(fn_name.to_string());
591                if path_parts.is_empty() {
592                    None
593                } else {
594                    Some(path_parts.join("::"))
595                }
596            };
597
598            // Test functions with regex pattern matching
599            if test_macro_regex.is_match(trimmed) {
600                // Look for the function on the next line(s)
601                for i in 1..=3 {
602                    if line_num + i < lines.len() {
603                        let next_line = lines[line_num + i];
604                        if let Some(fn_name) = Self::extract_function_name(next_line) {
605                            let full_path = build_module_path(&fn_name);
606                            entry_points.push(EntryPoint {
607                                name: fn_name.clone(),
608                                entry_type: EntryPointType::Test,
609                                line: line_num as u32 + i as u32 + 1,
610                                column: 0,
611                                line_range: (line_num as u32 + 1, line_num as u32 + 1),
612                                full_path,
613                            });
614                            break;
615                        }
616                    }
617                }
618            } else if (trimmed.starts_with("fn test_") || trimmed.contains("fn test_"))
619                && (in_test_module || trimmed.contains("#["))
620            {
621                if let Some(fn_name) = Self::extract_function_name(trimmed) {
622                    let column = line.find(&format!("fn {fn_name}")).unwrap_or(0) as u32;
623                    let full_path = build_module_path(&fn_name);
624                    entry_points.push(EntryPoint {
625                        name: fn_name.clone(),
626                        entry_type: EntryPointType::Test,
627                        line: line_num as u32 + 1,
628                        column,
629                        line_range: (line_num as u32 + 1, line_num as u32 + 1),
630                        full_path,
631                    });
632                }
633            }
634
635            // Benchmark functions
636            if trimmed.starts_with("#[bench]") {
637                if let Some(next_line) = lines.get(line_num + 1) {
638                    if let Some(fn_name) = Self::extract_function_name(next_line) {
639                        entry_points.push(EntryPoint {
640                            name: fn_name,
641                            entry_type: EntryPointType::Benchmark,
642                            line: line_num as u32 + 2,
643                            column: 0,
644                            line_range: (line_num as u32 + 1, line_num as u32 + 1),
645                            full_path: None,
646                        });
647                    }
648                }
649            }
650
651            // criterion benchmarks
652            if trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!") {
653                entry_points.push(EntryPoint {
654                    name: "criterion_bench".to_string(),
655                    entry_type: EntryPointType::Benchmark,
656                    line: line_num as u32 + 1,
657                    column: 0,
658                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
659                    full_path: None,
660                });
661            }
662        }
663
664        // Final check: if we're still in a doc comment at the end of the loop and cursor is in it
665        // This handles the case where the file ends with a doc comment
666        if in_doc_comment && cursor.is_some() {
667            let cursor_pos = cursor.unwrap();
668            if let Some(start) = doc_test_start {
669                if cursor_pos.line + 1 >= start {
670                    // The doc comment goes to the end of the file, so we can't find a function after it
671                    // Use a generic name for now
672                    entry_points.push(EntryPoint {
673                        name: "doc_test".to_string(),
674                        entry_type: EntryPointType::DocTest,
675                        line: start,
676                        column: 0,
677                        line_range: (
678                            doc_comment_start_line.unwrap_or(0) as u32 + 1,
679                            lines.len() as u32,
680                        ),
681                        full_path: None,
682                    });
683                }
684            }
685        }
686
687        // Add test module entry if cursor is in a test module but not on a specific test
688        if let Some(cursor_pos) = cursor {
689            if in_test_module && !module_stack.is_empty() {
690                // Check if cursor is in test module but not directly on a test function
691                let cursor_line = cursor_pos.line + 1;
692                let on_specific_test = entry_points
693                    .iter()
694                    .any(|ep| ep.entry_type == EntryPointType::Test && ep.line == cursor_line);
695
696                if !on_specific_test {
697                    let mut path_parts = file_module_path.clone();
698                    path_parts.extend(module_stack.clone());
699                    let current_module_path = path_parts.join("::");
700                    entry_points.push(EntryPoint {
701                        name: format!(
702                            "{}_module",
703                            module_stack.last().unwrap_or(&"tests".to_string())
704                        ),
705                        entry_type: EntryPointType::TestModule,
706                        line: cursor_line,
707                        column: 0,
708                        line_range: (cursor_line, cursor_line),
709                        full_path: Some(current_module_path),
710                    });
711                }
712            }
713        }
714
715        Ok(entry_points)
716    }
717
718    /// Determine execution capabilities
719    fn determine_capabilities(
720        project_type: &RustProjectType,
721        file_role: &FileRole,
722        entry_points: &[EntryPoint],
723    ) -> ExecutionCapabilities {
724        let has_main = entry_points
725            .iter()
726            .any(|ep| ep.entry_type == EntryPointType::Main);
727        let has_tests = entry_points
728            .iter()
729            .any(|ep| ep.entry_type == EntryPointType::Test);
730        let has_benches = entry_points
731            .iter()
732            .any(|ep| ep.entry_type == EntryPointType::Benchmark);
733
734        match file_role {
735            FileRole::MainBinary { .. } | FileRole::AdditionalBinary { .. } => {
736                ExecutionCapabilities {
737                    can_run: has_main,
738                    can_test: has_tests,
739                    can_bench: has_benches,
740                    can_doc_test: false,
741                    requires_framework: None,
742                }
743            }
744            FileRole::FrontendLibrary { framework } => ExecutionCapabilities {
745                can_run: true, // Framework-specific run
746                can_test: has_tests,
747                can_bench: has_benches,
748                can_doc_test: true,
749                requires_framework: Some(framework.clone()),
750            },
751            FileRole::LibraryRoot => ExecutionCapabilities {
752                can_run: false,
753                can_test: has_tests,
754                can_bench: has_benches,
755                can_doc_test: true,
756                requires_framework: None,
757            },
758            FileRole::IntegrationTest { .. } => ExecutionCapabilities {
759                can_run: false,
760                can_test: true,
761                can_bench: false,
762                can_doc_test: false,
763                requires_framework: None,
764            },
765            FileRole::Benchmark { .. } => ExecutionCapabilities {
766                can_run: false,
767                can_test: false,
768                can_bench: true,
769                can_doc_test: false,
770                requires_framework: None,
771            },
772            FileRole::Example { .. } => ExecutionCapabilities {
773                can_run: has_main,
774                can_test: has_tests,
775                can_bench: has_benches,
776                can_doc_test: false,
777                requires_framework: None,
778            },
779            FileRole::BuildScript => ExecutionCapabilities {
780                can_run: has_main,
781                can_test: false,
782                can_bench: false,
783                can_doc_test: false,
784                requires_framework: None,
785            },
786            FileRole::Standalone => match project_type {
787                RustProjectType::SingleFile { file_type, .. } => match file_type {
788                    SingleFileType::Executable => ExecutionCapabilities {
789                        can_run: has_main,
790                        can_test: has_tests,
791                        can_bench: has_benches,
792                        can_doc_test: false,
793                        requires_framework: None,
794                    },
795                    SingleFileType::Library => ExecutionCapabilities {
796                        can_run: false,
797                        can_test: has_tests,
798                        can_bench: has_benches,
799                        can_doc_test: true,
800                        requires_framework: None,
801                    },
802                    SingleFileType::Test => ExecutionCapabilities {
803                        can_run: false,
804                        can_test: true,
805                        can_bench: false,
806                        can_doc_test: false,
807                        requires_framework: None,
808                    },
809                    SingleFileType::Module => ExecutionCapabilities {
810                        can_run: false,
811                        can_test: has_tests,
812                        can_bench: false,
813                        can_doc_test: false,
814                        requires_framework: None,
815                    },
816                },
817                RustProjectType::CargoScript { .. } => ExecutionCapabilities {
818                    can_run: has_main,
819                    can_test: has_tests,
820                    can_bench: has_benches,
821                    can_doc_test: false,
822                    requires_framework: None,
823                },
824                _ => {
825                    // For standalone files in cargo projects, determine capabilities based on content
826                    ExecutionCapabilities {
827                        can_run: has_main,
828                        can_test: has_tests,
829                        can_bench: has_benches,
830                        can_doc_test: false,
831                        requires_framework: None,
832                    }
833                }
834            },
835            FileRole::Module => ExecutionCapabilities {
836                can_run: false,
837                can_test: has_tests,
838                can_bench: has_benches,
839                can_doc_test: false,
840                requires_framework: None,
841            },
842        }
843    }
844
845    // Helper methods
846
847    fn is_cargo_script(content: &str) -> bool {
848        content.starts_with("#!/usr/bin/env -S cargo")
849            || content.starts_with("#!/usr/bin/env cargo")
850            || content.starts_with("#!/usr/bin/env cargo-eval")
851            || content.starts_with("#!/usr/bin/env run-cargo-script")
852    }
853
854    fn extract_cargo_script_manifest(content: &str) -> Option<String> {
855        // Look for embedded manifest
856        if let Some(start) = content.find("//! ```cargo") {
857            let search_start = start + 13; // Length of "//! ```cargo"
858            if let Some(relative_end) = content[search_start..].find("//! ```") {
859                let end = search_start + relative_end;
860                return Some(content[search_start..end].trim().to_string());
861            }
862        }
863        None
864    }
865
866    fn parse_workspace_members(content: &str, root: &Path) -> RazResult<Vec<WorkspaceMember>> {
867        let mut members = Vec::new();
868
869        // Simple parsing - in a real implementation, use a TOML parser
870        if let Some(members_section) = content.find("members = [") {
871            let section = &content[members_section..];
872            if let Some(end) = section.find(']') {
873                let members_str = &section[11..end];
874                for member in members_str.split(',') {
875                    let member_name = member.trim().trim_matches('"').trim_matches('\'');
876                    if !member_name.is_empty() {
877                        members.push(WorkspaceMember {
878                            name: member_name.to_string(),
879                            path: root.join(member_name),
880                            is_current: false, // Will be determined later
881                        });
882                    }
883                }
884            }
885        }
886
887        Ok(members)
888    }
889
890    fn extract_package_name(content: &str) -> RazResult<String> {
891        if let Some(name_start) = content.find("name = \"") {
892            let name_section = &content[name_start + 8..];
893            if let Some(name_end) = name_section.find('"') {
894                return Ok(name_section[..name_end].to_string());
895            }
896        }
897        Ok("unknown".to_string())
898    }
899
900    fn extract_binary_name_from_path(file_path: &Path) -> String {
901        // For src/main.rs, try to determine package name from Cargo.toml
902        if let Some(parent) = file_path.parent() {
903            if let Some(cargo_dir) = parent.parent() {
904                let cargo_toml = cargo_dir.join("Cargo.toml");
905                if cargo_toml.exists() {
906                    if let Ok(content) = fs::read_to_string(&cargo_toml) {
907                        if let Ok(name) = Self::extract_package_name(&content) {
908                            return name;
909                        }
910                    }
911                }
912            }
913        }
914        "main".to_string()
915    }
916
917    fn is_frontend_library(file_path: &Path) -> bool {
918        let path_str = file_path.to_string_lossy();
919        let file_name = file_path.file_name().unwrap_or_default().to_string_lossy();
920
921        file_name == "lib.rs"
922            && (path_str.contains("/frontend/")
923                || path_str.contains("/app/")
924                || path_str.contains("/client/")
925                || path_str.contains("/web/"))
926    }
927
928    fn detect_web_framework(file_path: &Path) -> RazResult<String> {
929        // Walk up to find Cargo.toml and check dependencies
930        let mut current = file_path;
931        while let Some(parent) = current.parent() {
932            let cargo_toml = parent.join("Cargo.toml");
933            if cargo_toml.exists() {
934                if let Ok(content) = fs::read_to_string(&cargo_toml) {
935                    if content.contains("leptos") {
936                        return Ok("leptos".to_string());
937                    } else if content.contains("dioxus") {
938                        return Ok("dioxus".to_string());
939                    } else if content.contains("yew") {
940                        return Ok("yew".to_string());
941                    }
942                }
943            }
944            current = parent;
945        }
946        Ok("unknown".to_string())
947    }
948
949    fn detect_non_test_entries(content: &str) -> RazResult<Vec<EntryPoint>> {
950        let mut entry_points = Vec::new();
951        let lines: Vec<&str> = content.lines().collect();
952        let mut in_doc_comment = false;
953
954        for (line_num, line) in lines.iter().enumerate() {
955            let trimmed = line.trim();
956
957            // Track doc comments to avoid false positives
958            if trimmed.starts_with("///") || trimmed.starts_with("//!") {
959                in_doc_comment = true;
960            } else if in_doc_comment && !trimmed.starts_with("//") {
961                in_doc_comment = false;
962            }
963
964            // Main function - only detect if not in doc comment
965            if !in_doc_comment && trimmed.contains("fn main(") {
966                entry_points.push(EntryPoint {
967                    name: "main".to_string(),
968                    entry_type: EntryPointType::Main,
969                    line: line_num as u32 + 1,
970                    column: line.find("fn main(").unwrap_or(0) as u32,
971                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
972                    full_path: None,
973                });
974            }
975
976            // Benchmark functions - only detect if not in doc comment
977            if !in_doc_comment && trimmed.starts_with("#[bench]") {
978                if let Some(next_line) = lines.get(line_num + 1) {
979                    if let Some(fn_name) = Self::extract_function_name(next_line) {
980                        entry_points.push(EntryPoint {
981                            name: fn_name,
982                            entry_type: EntryPointType::Benchmark,
983                            line: line_num as u32 + 2,
984                            column: 0,
985                            line_range: (line_num as u32 + 1, line_num as u32 + 1),
986                            full_path: None,
987                        });
988                    }
989                }
990            }
991
992            // criterion benchmarks - only detect if not in doc comment
993            if !in_doc_comment
994                && (trimmed.contains("criterion_group!") || trimmed.contains("criterion_main!"))
995            {
996                entry_points.push(EntryPoint {
997                    name: "criterion_bench".to_string(),
998                    entry_type: EntryPointType::Benchmark,
999                    line: line_num as u32 + 1,
1000                    column: 0,
1001                    line_range: (line_num as u32 + 1, line_num as u32 + 1),
1002                    full_path: None,
1003                });
1004            }
1005        }
1006
1007        Ok(entry_points)
1008    }
1009
1010    fn extract_function_name(line: &str) -> Option<String> {
1011        let trimmed = line.trim();
1012        if let Some(fn_start) = trimmed.find("fn ") {
1013            let fn_part = &trimmed[fn_start + 3..];
1014            if let Some(paren_pos) = fn_part.find('(') {
1015                let fn_name = fn_part[..paren_pos].trim();
1016                if !fn_name.is_empty() {
1017                    return Some(fn_name.to_string());
1018                }
1019            }
1020        }
1021        None
1022    }
1023
1024    fn extract_struct_name(line: &str) -> Option<String> {
1025        let trimmed = line.trim();
1026        let struct_start = if trimmed.starts_with("pub struct ") {
1027            11 // Length of "pub struct "
1028        } else if trimmed.starts_with("struct ") {
1029            7 // Length of "struct "
1030        } else {
1031            return None;
1032        };
1033
1034        let after_struct = &trimmed[struct_start..];
1035        // Find the end of the struct name (space, <, {, or ;)
1036        let name_end = after_struct
1037            .find([' ', '<', '{', ';'])
1038            .unwrap_or(after_struct.len());
1039        let struct_name = after_struct[..name_end].trim();
1040
1041        if !struct_name.is_empty() {
1042            Some(struct_name.to_string())
1043        } else {
1044            None
1045        }
1046    }
1047
1048    fn extract_impl_name(line: &str) -> Option<String> {
1049        let trimmed = line.trim();
1050        if !trimmed.starts_with("impl") {
1051            return None;
1052        }
1053
1054        // Handle "impl Trait for Type" or "impl Type"
1055        if let Some(for_pos) = trimmed.find(" for ") {
1056            // Extract the type after "for"
1057            let after_for = &trimmed[for_pos + 5..];
1058            let name_end = after_for.find([' ', '{', '<']).unwrap_or(after_for.len());
1059            let type_name = after_for[..name_end].trim();
1060            if !type_name.is_empty() {
1061                return Some(type_name.to_string());
1062            }
1063        } else {
1064            // Handle "impl Type" or "impl<T> Type"
1065            let after_impl = if trimmed.starts_with("impl<") {
1066                // Skip generic parameters
1067                if let Some(gt_pos) = trimmed.find('>') {
1068                    &trimmed[gt_pos + 1..].trim()
1069                } else {
1070                    return None;
1071                }
1072            } else {
1073                &trimmed[4..].trim() // Skip "impl"
1074            };
1075
1076            let name_end = after_impl.find([' ', '{', '<']).unwrap_or(after_impl.len());
1077            let type_name = after_impl[..name_end].trim();
1078            if !type_name.is_empty() {
1079                return Some(type_name.to_string());
1080            }
1081        }
1082        None
1083    }
1084
1085    /// Build module path from file path relative to src/
1086    fn build_module_path_from_file(
1087        file_path: &Path,
1088        project_type: &RustProjectType,
1089    ) -> Vec<String> {
1090        let mut module_path = Vec::new();
1091
1092        // Only build module paths for cargo projects
1093        let src_dir = match project_type {
1094            RustProjectType::CargoWorkspace { root, .. } => root.join("src"),
1095            RustProjectType::CargoPackage { root, .. } => root.join("src"),
1096            _ => return module_path,
1097        };
1098
1099        // Get the relative path from src/
1100        if let Ok(relative_path) = file_path.strip_prefix(&src_dir) {
1101            // Convert path components to module names
1102            for component in relative_path.components() {
1103                if let std::path::Component::Normal(os_str) = component {
1104                    if let Some(name) = os_str.to_str() {
1105                        // Remove .rs extension and handle special names
1106                        let module_name = if name == "lib.rs" || name == "main.rs" {
1107                            continue; // lib.rs and main.rs are root modules
1108                        } else if name == "mod.rs" {
1109                            // mod.rs represents the parent directory name
1110                            if let Some(parent) = relative_path.parent() {
1111                                if let Some(parent_name) = parent.file_name() {
1112                                    parent_name.to_str().unwrap_or("").to_string()
1113                                } else {
1114                                    continue;
1115                                }
1116                            } else {
1117                                continue;
1118                            }
1119                        } else if name.ends_with(".rs") {
1120                            name.trim_end_matches(".rs").to_string()
1121                        } else {
1122                            // Directory name
1123                            name.to_string()
1124                        };
1125
1126                        if !module_name.is_empty() {
1127                            module_path.push(module_name);
1128                        }
1129                    }
1130                }
1131            }
1132        }
1133
1134        module_path
1135    }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141    use tempfile::TempDir;
1142
1143    #[test]
1144    fn test_cargo_script_detection() {
1145        let content = "#!/usr/bin/env -S cargo +nightly -Zscript\n\nfn main() {}";
1146        assert!(FileDetector::is_cargo_script(content));
1147    }
1148
1149    #[test]
1150    fn test_function_name_extraction() {
1151        assert_eq!(
1152            FileDetector::extract_function_name("fn test_something() {"),
1153            Some("test_something".to_string())
1154        );
1155        assert_eq!(
1156            FileDetector::extract_function_name("    fn main() {"),
1157            Some("main".to_string())
1158        );
1159    }
1160
1161    #[test]
1162    fn test_frontend_library_detection() {
1163        let temp_dir = TempDir::new().unwrap();
1164        let frontend_lib = temp_dir.path().join("frontend").join("src").join("lib.rs");
1165        assert!(FileDetector::is_frontend_library(&frontend_lib));
1166
1167        let regular_lib = temp_dir.path().join("src").join("lib.rs");
1168        assert!(!FileDetector::is_frontend_library(&regular_lib));
1169    }
1170
1171    #[test]
1172    fn test_nested_module_path_resolution() -> RazResult<()> {
1173        let temp_dir = TempDir::new()?;
1174        let test_file = temp_dir.path().join("nested_test.rs");
1175
1176        let content = r#"
1177#[cfg(test)]
1178mod tests {
1179    use super::*;
1180
1181    #[test]
1182    fn test_top_level() {
1183        assert_eq!(2 + 2, 4);
1184    }
1185
1186    mod integration {
1187        use super::*;
1188
1189        #[test] 
1190        fn test_integration_basic() {
1191            assert!(true);
1192        }
1193
1194        mod database {
1195            use super::*;
1196
1197            #[test]
1198            fn test_db_connection() {
1199                assert!(true);
1200            }
1201
1202            mod transactions {
1203                use super::*;
1204
1205                #[test]
1206                fn test_transaction_rollback() {
1207                    assert!(true);
1208                }
1209            }
1210        }
1211    }
1212}
1213"#;
1214
1215        fs::write(&test_file, content)?;
1216
1217        // Test detecting nested test at deeply nested level (line 28 - test_transaction_rollback)
1218        let context = FileDetector::detect_context(
1219            &test_file,
1220            Some(Position {
1221                line: 27,
1222                column: 1,
1223            }),
1224        )?;
1225
1226        // Find the deeply nested test
1227        let nested_test = context.entry_points.iter().find(|ep| {
1228            ep.name == "test_transaction_rollback" && ep.entry_type == EntryPointType::Test
1229        });
1230
1231        assert!(
1232            nested_test.is_some(),
1233            "Should find the deeply nested test function"
1234        );
1235
1236        let test_entry = nested_test.unwrap();
1237        assert_eq!(
1238            test_entry.full_path.as_ref().unwrap(),
1239            "tests::integration::database::transactions::test_transaction_rollback",
1240            "Should have correct full module path for deeply nested test"
1241        );
1242
1243        // Test detecting test at intermediate level (line 20 - test_db_connection)
1244        let context2 = FileDetector::detect_context(
1245            &test_file,
1246            Some(Position {
1247                line: 19,
1248                column: 1,
1249            }),
1250        )?;
1251
1252        let mid_level_test = context2
1253            .entry_points
1254            .iter()
1255            .find(|ep| ep.name == "test_db_connection" && ep.entry_type == EntryPointType::Test);
1256
1257        assert!(
1258            mid_level_test.is_some(),
1259            "Should find the mid-level nested test"
1260        );
1261
1262        let test_entry2 = mid_level_test.unwrap();
1263        assert_eq!(
1264            test_entry2.full_path.as_ref().unwrap(),
1265            "tests::integration::database::test_db_connection",
1266            "Should have correct full module path for mid-level nested test"
1267        );
1268
1269        // Test detecting top-level test
1270        let top_level_test = context
1271            .entry_points
1272            .iter()
1273            .find(|ep| ep.name == "test_top_level" && ep.entry_type == EntryPointType::Test);
1274
1275        assert!(top_level_test.is_some(), "Should find the top-level test");
1276
1277        let test_entry3 = top_level_test.unwrap();
1278        assert_eq!(
1279            test_entry3.full_path.as_ref().unwrap(),
1280            "tests::test_top_level",
1281            "Should have correct module path for top-level test"
1282        );
1283
1284        Ok(())
1285    }
1286
1287    #[test]
1288    fn test_module_stack_handling_with_complex_nesting() -> RazResult<()> {
1289        let temp_dir = TempDir::new()?;
1290        let test_file = temp_dir.path().join("complex_nested.rs");
1291
1292        let content = r#"
1293mod outer {
1294    mod middle {
1295        #[cfg(test)]
1296        mod tests {
1297            #[test]
1298            fn test_deeply_nested() {
1299                assert!(true);
1300            }
1301            
1302            mod sub_tests {
1303                #[test]
1304                fn test_sub_nested() {
1305                    assert!(true);
1306                }
1307            }
1308        }
1309        
1310        mod other {
1311            fn regular_function() {}
1312        }
1313    }
1314}
1315
1316#[cfg(test)]
1317mod main_tests {
1318    #[test]
1319    fn test_main_level() {
1320        assert!(true);
1321    }
1322}
1323"#;
1324
1325        fs::write(&test_file, content)?;
1326
1327        let context = FileDetector::detect_context(&test_file, None)?;
1328
1329        // Check that we correctly detect tests in different nested locations
1330        let deeply_nested = context
1331            .entry_points
1332            .iter()
1333            .find(|ep| ep.name == "test_deeply_nested");
1334
1335        assert!(deeply_nested.is_some(), "Should find deeply nested test");
1336        assert_eq!(
1337            deeply_nested.unwrap().full_path.as_ref().unwrap(),
1338            "outer::middle::tests::test_deeply_nested",
1339            "Should correctly build path through multiple modules"
1340        );
1341
1342        let sub_nested = context
1343            .entry_points
1344            .iter()
1345            .find(|ep| ep.name == "test_sub_nested");
1346
1347        assert!(sub_nested.is_some(), "Should find sub-nested test");
1348        assert_eq!(
1349            sub_nested.unwrap().full_path.as_ref().unwrap(),
1350            "outer::middle::tests::sub_tests::test_sub_nested",
1351            "Should correctly build path through all nested modules"
1352        );
1353
1354        let main_test = context
1355            .entry_points
1356            .iter()
1357            .find(|ep| ep.name == "test_main_level");
1358
1359        assert!(main_test.is_some(), "Should find main level test");
1360        assert_eq!(
1361            main_test.unwrap().full_path.as_ref().unwrap(),
1362            "main_tests::test_main_level",
1363            "Should correctly build path for main level test"
1364        );
1365
1366        Ok(())
1367    }
1368}