## Coverage Report

#### Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/ruchy/src/backend/module_resolver.rs

    Line
    Count
    Source
    1
    //! Module resolver for multi-file imports
    2
    //!
    3
    //! This module provides functionality to resolve file imports in Ruchy programs
    4
    //! by pre-processing the AST to inline external modules before transpilation.
    5
    //!
    6
    //! # Architecture
    7
    //!
    8
    //! The module resolver works as a pre-processing step before transpilation:
    9
    //! 1. Parse the main file into an AST
    10
    //! 2. Scan for file imports (`use module_name;` where `module_name` has no `::`)
    11
    //! 3. Load and parse external module files
    12
    //! 4. Replace Import nodes with inline Module nodes
    13
    //! 5. Pass the resolved AST to the transpiler
    14
    //!
    15
    //! # Usage
    16
    //!
    17
    //! ```rust
    18
    //! use ruchy::{ModuleResolver, Parser, Transpiler};
    19
    //!
    20
    //! let mut resolver = ModuleResolver::new();
    21
    //! resolver.add_search_path("./src");
    22
    //!
    23
    //! let mut parser = Parser::new("use math; math::add(1, 2)");
    24
    //! let ast = parser.parse()?;
    25
    //!
    26
    //! let resolved_ast = resolver.resolve_imports(ast)?;
    27
    //!
    28
    //! let transpiler = Transpiler::new();
    29
    //! let rust_code = transpiler.transpile(&resolved_ast)?;
    30
    //! ```
    31
    32
    use crate::frontend::ast::{Expr, ExprKind, ImportItem, Span};
    33
    use crate::backend::module_loader::ModuleLoader;
    34
    use anyhow::{Result, Context};
    35
    36
    /// Module resolver for processing file imports
    37
    ///
    38
    /// Resolves file imports by loading external modules and inlining them
    39
    /// as Module declarations in the AST before transpilation.
    40
    pub struct ModuleResolver {
    41
        /// Module loader for file system operations
    42
        pub(crate) module_loader: ModuleLoader,
    43
    }
    44
    45
    impl ModuleResolver {
    46
        /// Create a new module resolver with default search paths
    47
        ///
    48
        /// Default search paths:
    49
        /// - `.` (current directory)
    50
        /// - `./src` (source directory)
    51
        /// - `./modules` (modules directory)
    52
        #[must_use]
    53
    0
        pub fn new() -> Self {
    54
    0
            Self {
    55
    0
                module_loader: ModuleLoader::new(),
    56
    0
            }
    57
    0
        }
    58
    59
        /// Add a directory to the module search path
    60
        ///
    61
        /// # Arguments
    62
        ///
    63
        /// * `path` - Directory to search for modules
    64
    0
        pub fn add_search_path<P: AsRef<std::path::Path>>(&mut self, path: P) {
    65
    0
            self.module_loader.add_search_path(path);
    66
    0
        }
    67
    68
        /// Resolve all file imports in an AST
    69
        ///
    70
        /// Recursively processes the AST to find file imports, loads the corresponding
    71
        /// modules, and replaces Import nodes with inline Module nodes.
    72
        ///
    73
        /// # Arguments
    74
        ///
    75
        /// * `ast` - The AST to process
    76
        ///
    77
        /// # Returns
    78
        ///
    79
        /// A new AST with all file imports resolved to inline modules
    80
        ///
    81
        /// # Errors
    82
        ///
    83
        /// Returns an error if:
    84
        /// - Module files cannot be found or loaded
    85
        /// - Module files contain invalid syntax
    86
        /// - Circular dependencies are detected
    87
    0
        pub fn resolve_imports(&mut self, ast: Expr) -> Result<Expr> {
    88
    0
            self.resolve_expr(ast)
    89
    0
        }
    90
    91
        /// Recursively resolve imports in an expression
    92
    0
        fn resolve_expr(&mut self, expr: Expr) -> Result<Expr> {
    93
    0
            match expr.kind {
    94
    0
                ExprKind::Import { ref path, ref items } => {
    95
                    // Check if this is a file import (no :: and not std:: or http)
    96
    0
                    if self.is_file_import(path) {
    97
                        // Load the module file
    98
    0
                        let parsed_module = self.module_loader.load_module(path)
    99
    0
                            .with_context(|| format!("Failed to resolve import '{path}'"))?;
    100
    101
                        // Recursively resolve imports in the loaded module
    102
    0
                        let resolved_module_ast = self.resolve_expr(parsed_module.ast)?;
    103
    104
                        // Create an inline module declaration
    105
    0
                        let module_expr = Expr::new(
    106
    0
                            ExprKind::Module {
    107
    0
                                name: path.clone(),
    108
    0
                                body: Box::new(resolved_module_ast),
    109
    0
                            },
    110
    0
                            expr.span,
    111
                        );
    112
    113
                        // Create a use statement to import from the module
    114
    0
                        let use_statement = if items.iter().any(|item| matches!(item, ImportItem::Wildcard)) || items.is_empty() {
    115
                            // Wildcard import: use module::*;
    116
    0
                            Expr::new(
    117
    0
                                ExprKind::Import {
    118
    0
                                    path: path.clone(),
    119
    0
                                    items: vec![ImportItem::Wildcard],
    120
    0
                                },
    121
    0
                                Span { start: 0, end: 0 },
    122
                            )
    123
                        } else {
    124
                            // Specific imports: use module::{item1, item2};
    125
    0
                            self.create_use_statements(path, items)
    126
                        };
    127
    128
                        // Return a block with the module declaration and use statement
    129
    0
                        Ok(Expr::new(
    130
    0
                            ExprKind::Block(vec![module_expr, use_statement]),
    131
    0
                            expr.span,
    132
    0
                        ))
    133
                    } else {
    134
                        // Not a file import, keep as-is
    135
    0
                        Ok(expr)
    136
                    }
    137
                }
    138
    0
                ExprKind::Block(exprs) => {
    139
                    // Resolve imports in all block expressions
    140
    0
                    let resolved_exprs: Result<Vec<_>> = exprs
    141
    0
                        .into_iter()
    142
    0
                        .map(|e| self.resolve_expr(e))
    143
    0
                        .collect();
    144
    0
                    Ok(Expr::new(ExprKind::Block(resolved_exprs?), expr.span))
    145
                }
    146
    0
                ExprKind::Module { name, body } => {
    147
                    // Resolve imports in module body
    148
    0
                    let resolved_body = self.resolve_expr(*body)?;
    149
    0
                    Ok(Expr::new(
    150
    0
                        ExprKind::Module {
    151
    0
                            name,
    152
    0
                            body: Box::new(resolved_body),
    153
    0
                        },
    154
    0
                        expr.span,
    155
    0
                    ))
    156
                }
    157
                ExprKind::Function {
    158
    0
                    name,
    159
    0
                    type_params,
    160
    0
                    params,
    161
    0
                    body,
    162
    0
                    is_async,
    163
    0
                    return_type,
    164
    0
                    is_pub,
    165
                } => {
    166
                    // Resolve imports in function body
    167
    0
                    let resolved_body = self.resolve_expr(*body)?;
    168
    0
                    Ok(Expr::new(
    169
    0
                        ExprKind::Function {
    170
    0
                            name,
    171
    0
                            type_params,
    172
    0
                            params,
    173
    0
                            body: Box::new(resolved_body),
    174
    0
                            is_async,
    175
    0
                            return_type,
    176
    0
                            is_pub,
    177
    0
                        },
    178
    0
                        expr.span,
    179
    0
                    ))
    180
                }
    181
    0
                ExprKind::If { condition, then_branch, else_branch } => {
    182
    0
                    let resolved_condition = self.resolve_expr(*condition)?;
    183
    0
                    let resolved_then = self.resolve_expr(*then_branch)?;
    184
    0
                    let resolved_else = else_branch.map(|e| self.resolve_expr(*e)).transpose()?;
    185
    0
                    Ok(Expr::new(
    186
    0
                        ExprKind::If {
    187
    0
                            condition: Box::new(resolved_condition),
    188
    0
                            then_branch: Box::new(resolved_then),
    189
    0
                            else_branch: resolved_else.map(Box::new),
    190
    0
                        },
    191
    0
                        expr.span,
    192
    0
                    ))
    193
                }
    194
                // For other expression types, recursively process children as needed
    195
                // For now, just return the expression as-is
    196
    0
                _ => Ok(expr),
    197
            }
    198
    0
        }
    199
    200
        /// Check if an import path represents a file import
    201
    0
        fn is_file_import(&self, path: &str) -> bool {
    202
    0
            !path.contains("::")
    203
    0
                && !path.starts_with("std::")
    204
    0
                && !path.starts_with("http")
    205
    0
                && !path.is_empty()
    206
    0
        }
    207
    208
        /// Create use statements for specific imports
    209
    0
        fn create_use_statements(&self, module_path: &str, items: &[ImportItem]) -> Expr {
    210
            // Create a use statement that imports specific items from the module
    211
            // This will be transpiled to proper Rust use statements
    212
    0
            Expr::new(
    213
    0
                ExprKind::Import {
    214
    0
                    path: module_path.to_string(), // Use the module path as-is
    215
    0
                    items: items.to_vec(),
    216
    0
                },
    217
    0
                Span { start: 0, end: 0 },
    218
            )
    219
    0
        }
    220
    221
        /// Get module loading statistics
    222
        #[must_use]
    223
    0
        pub fn stats(&self) -> crate::backend::module_loader::ModuleLoaderStats {
    224
    0
            self.module_loader.stats()
    225
    0
        }
    226
    227
        /// Clear the module cache
    228
        ///
    229
        /// Forces all modules to be reloaded from disk on next access.
    230
    0
        pub fn clear_cache(&mut self) {
    231
    0
            self.module_loader.clear_cache();
    232
    0
        }
    233
    }
    234
    235
    impl Default for ModuleResolver {
    236
    0
        fn default() -> Self {
    237
    0
            Self::new()
    238
    0
        }
    239
    }
    240
    241
    #[cfg(test)]
    242
    mod tests {
    243
        use super::*;
    244
        use tempfile::TempDir;
    245
        use std::fs;
    246
        use crate::frontend::ast::Literal;
    247
    248
        fn create_test_module(temp_dir: &TempDir, name: &str, content: &str) -> Result<()> {
    249
            let file_path = temp_dir.path().join(format!("{name}.ruchy"));
    250
            fs::write(file_path, content)?;
    251
            Ok(())
    252
        }
    253
    254
        #[test]
    255
        fn test_module_resolver_creation() {
    256
            let resolver = ModuleResolver::new();
    257
            let stats = resolver.stats();
    258
            assert_eq!(stats.cached_modules, 0);
    259
        }
    260
    261
        #[test]
    262
        fn test_add_search_path() {
    263
            let mut resolver = ModuleResolver::new();
    264
            resolver.add_search_path("/custom/path");
    265
            // Module loader doesn't expose search paths, so we can't directly test this
    266
            // But we can test that it doesn't panic
    267
        }
    268
    269
        #[test]
    270
        fn test_is_file_import() {
    271
            let resolver = ModuleResolver::new();
    272
    273
            // Should be file imports
    274
            assert!(resolver.is_file_import("math"));
    275
            assert!(resolver.is_file_import("utils"));
    276
            assert!(resolver.is_file_import("snake_case_module"));
    277
    278
            // Should NOT be file imports
    279
            assert!(!resolver.is_file_import("std::collections"));
    280
            assert!(!resolver.is_file_import("std::io::Read"));
    281
            assert!(!resolver.is_file_import("https://example.com/module.ruchy"));
    282
            assert!(!resolver.is_file_import("http://localhost/module.ruchy"));
    283
            assert!(!resolver.is_file_import(""));
    284
        }
    285
    286
        #[test]
    287
        fn test_resolve_simple_file_import() -> Result<()> {
    288
            let temp_dir = TempDir::new()?;
    289
            let mut resolver = ModuleResolver::new();
    290
            resolver.add_search_path(temp_dir.path());
    291
    292
            // Create a simple math module
    293
            create_test_module(&temp_dir, "math", r"
    294
                pub fun add(a: i32, b: i32) -> i32 {
    295
                    a + b
    296
                }
    297
            ")?;
    298
    299
            // Create an import expression
    300
            let import_expr = Expr::new(
    301
                ExprKind::Import {
    302
                    path: "math".to_string(),
    303
                    items: vec![ImportItem::Wildcard],
    304
                },
    305
                Span { start: 0, end: 0 },
    306
            );
    307
    308
            // Resolve the import
    309
            let resolved_expr = resolver.resolve_imports(import_expr)?;
    310
    311
            // Should be converted to a Block with Module declaration and use statement
    312
            match resolved_expr.kind {
    313
                ExprKind::Block(exprs) => {
    314
                    assert_eq!(exprs.len(), 2);
    315
                    // First should be Module declaration
    316
                    match &exprs[0].kind {
    317
                        ExprKind::Module { name, .. } => {
    318
                            assert_eq!(name, "math");
    319
                        }
    320
                        _ => unreachable!("Expected first element to be Module, got {:?}", exprs[0].kind),
    321
                    }
    322
                    // Second should be use statement
    323
                    match &exprs[1].kind {
    324
                        ExprKind::Import { path, items } => {
    325
                            assert_eq!(path, "math");
    326
                            assert_eq!(items.len(), 1);
    327
                            assert!(matches!(items[0], ImportItem::Wildcard));
    328
                        }
    329
                        _ => unreachable!("Expected second element to be Import, got {:?}", exprs[1].kind),
    330
                    }
    331
                }
    332
                _ => unreachable!("Expected Block expression, got {:?}", resolved_expr.kind),
    333
            }
    334
    335
            Ok(())
    336
        }
    337
    338
        #[test]
    339
        fn test_resolve_non_file_import() -> Result<()> {
    340
            let mut resolver = ModuleResolver::new();
    341
    342
            // Create a standard library import
    343
            let import_expr = Expr::new(
    344
                ExprKind::Import {
    345
                    path: "std::collections".to_string(),
    346
                    items: vec![ImportItem::Named("HashMap".to_string())],
    347
                },
    348
                Span { start: 0, end: 0 },
    349
            );
    350
    351
            // Resolve the import - should remain unchanged
    352
            let resolved_expr = resolver.resolve_imports(import_expr)?;
    353
    354
            match resolved_expr.kind {
    355
                ExprKind::Import { path, items } => {
    356
                    assert_eq!(path, "std::collections");
    357
                    assert_eq!(items.len(), 1);
    358
                }
    359
                _ => unreachable!("Expected Import expression to remain unchanged"),
    360
            }
    361
    362
            Ok(())
    363
        }
    364
    365
        #[test]
    366
        fn test_resolve_block_with_imports() -> Result<()> {
    367
            let temp_dir = TempDir::new()?;
    368
            let mut resolver = ModuleResolver::new();
    369
            resolver.add_search_path(temp_dir.path());
    370
    371
            create_test_module(&temp_dir, "math", "42")?;
    372
    373
            // Create a block with mixed imports
    374
            let block_expr = Expr::new(
    375
                ExprKind::Block(vec![
    376
                    Expr::new(
    377
                        ExprKind::Import {
    378
                            path: "math".to_string(),
    379
                            items: vec![ImportItem::Wildcard],
    380
                        },
    381
                        Span { start: 0, end: 0 },
    382
                    ),
    383
                    Expr::new(
    384
                        ExprKind::Import {
    385
                            path: "std::io".to_string(),
    386
                            items: vec![ImportItem::Named("Read".to_string())],
    387
                        },
    388
                        Span { start: 0, end: 0 },
    389
                    ),
    390
                    Expr::new(
    391
                        ExprKind::Literal(Literal::Integer(42)),
    392
                        Span { start: 0, end: 0 },
    393
                    ),
    394
                ]),
    395
                Span { start: 0, end: 0 },
    396
            );
    397
    398
            let resolved_block = resolver.resolve_imports(block_expr)?;
    399
    400
            if let ExprKind::Block(exprs) = resolved_block.kind {
    401
                assert_eq!(exprs.len(), 3);
    402
    403
                // First should be Block containing Module and use statement (from file import)
    404
                match &exprs[0].kind {
    405
                    ExprKind::Block(inner_exprs) => {
    406
                        assert_eq!(inner_exprs.len(), 2);
    407
                        assert!(matches!(inner_exprs[0].kind, ExprKind::Module { .. }));
    408
                        assert!(matches!(inner_exprs[1].kind, ExprKind::Import { .. }));
    409
                    }
    410
                    _ => unreachable!("Expected first element to be Block, got {:?}", exprs[0].kind),
    411
                }
    412
    413
                // Second should remain as Import (std::io - not a file import)
    414
                assert!(matches!(exprs[1].kind, ExprKind::Import { .. }));
    415
    416
                // Third should remain as Literal
    417
                assert!(matches!(exprs[2].kind, ExprKind::Literal(Literal::Integer(42))));
    418
            } else {
    419
                unreachable!("Expected Block expression");
    420
            }
    421
    422
            Ok(())
    423
        }
    424
    425
        #[test]
    426
        fn test_stats_and_cache() -> Result<()> {
    427
            let temp_dir = TempDir::new()?;
    428
            let mut resolver = ModuleResolver::new();
    429
            resolver.module_loader.search_paths.clear(); // Remove default paths
    430
            resolver.add_search_path(temp_dir.path());
    431
    432
            create_test_module(&temp_dir, "test", "42")?;
    433
    434
            let initial_stats = resolver.stats();
    435
            assert_eq!(initial_stats.files_loaded, 0);
    436
    437
            // Load a module
    438
            let import_expr = Expr::new(
    439
                ExprKind::Import {
    440
                    path: "test".to_string(),
    441
                    items: vec![ImportItem::Wildcard],
    442
                },
    443
                Span { start: 0, end: 0 },
    444
            );
    445
    446
            resolver.resolve_imports(import_expr)?;
    447
    448
            let after_stats = resolver.stats();
    449
            assert_eq!(after_stats.files_loaded, 1);
    450
            assert_eq!(after_stats.cached_modules, 1);
    451
    452
            // Clear cache
    453
            resolver.clear_cache();
    454
            let cleared_stats = resolver.stats();
    455
            assert_eq!(cleared_stats.files_loaded, 0);
    456
            assert_eq!(cleared_stats.cached_modules, 0);
    457
    458
            Ok(())
    459
        }
    460
    }