gruel_air/sema/
visibility.rs

1//! Visibility checking for module system.
2//!
3//! This module implements the visibility rules defined in ADR-0026:
4//! - `pub` items are always accessible
5//! - Private items are accessible if the files are in the same directory module
6
7use std::path::{Path, PathBuf};
8
9use gruel_error::{CompileError, CompileResult, ErrorKind};
10use gruel_span::FileId;
11
12use crate::types::EnumId;
13
14use super::Sema;
15
16impl Sema<'_> {
17    /// Check if the accessing file can see a private item from the target file.
18    ///
19    /// Visibility rules (per ADR-0026):
20    /// - `pub` items are always accessible
21    /// - Private items are accessible if the files are in the same directory module
22    ///
23    /// Directory module membership includes:
24    /// - Files directly in the directory (e.g., `utils/strings.gruel` is in `utils`)
25    /// - Facade files for the directory (e.g., `_utils.gruel` is in `utils` module)
26    ///
27    /// Returns true if the item is accessible.
28    pub(crate) fn is_accessible(
29        &self,
30        accessing_file_id: FileId,
31        target_file_id: FileId,
32        is_pub: bool,
33    ) -> bool {
34        // Public items are always accessible
35        if is_pub {
36            return true;
37        }
38
39        // Get paths for both files
40        let accessing_path = self.get_file_path(accessing_file_id);
41        let target_path = self.get_file_path(target_file_id);
42
43        // If we can't determine the paths, be permissive (for single-file mode or tests)
44        match (accessing_path, target_path) {
45            (Some(acc), Some(tgt)) => {
46                // Get the "module identity" for each file.
47                // For a regular file like `utils/strings.gruel`, the module is `utils/`
48                // For a facade file like `_utils.gruel`, the module is `utils/` (the directory it represents)
49                let acc_module = get_module_identity(Path::new(acc));
50                let tgt_module = get_module_identity(Path::new(tgt));
51
52                acc_module == tgt_module
53            }
54            // If either path is unknown, allow access (e.g., synthetic types, single-file mode)
55            _ => true,
56        }
57    }
58
59    /// Resolve an enum type through a module reference.
60    ///
61    /// Used for qualified enum paths like `module.EnumName::Variant` in match patterns.
62    /// Checks visibility: private enums are only accessible from the same directory.
63    pub fn resolve_enum_through_module(
64        &self,
65        _module_ref: gruel_rir::InstRef,
66        type_name: lasso::Spur,
67        span: gruel_span::Span,
68    ) -> CompileResult<EnumId> {
69        let type_name_str = self.interner.resolve(&type_name);
70
71        // Try to find the enum globally
72        let enum_id = self.enums.get(&type_name).copied().ok_or_else(|| {
73            CompileError::new(ErrorKind::UnknownEnumType(type_name_str.to_string()), span)
74        })?;
75
76        // Check visibility
77        let enum_def = self.type_pool.enum_def(enum_id);
78        let accessing_file_id = span.file_id;
79        let target_file_id = enum_def.file_id;
80
81        if !self.is_accessible(accessing_file_id, target_file_id, enum_def.is_pub) {
82            return Err(CompileError::new(
83                ErrorKind::PrivateMemberAccess {
84                    item_kind: "enum".to_string(),
85                    name: type_name_str.to_string(),
86                },
87                span,
88            ));
89        }
90
91        Ok(enum_id)
92    }
93}
94
95/// Get the module identity for a file path.
96///
97/// - For regular files: returns the parent directory
98/// - For facade files (`_foo.gruel`): returns the corresponding directory (`foo/`)
99///
100/// This allows facade files to be treated as part of their corresponding directory module.
101pub(crate) fn get_module_identity(path: &Path) -> Option<PathBuf> {
102    let parent = path.parent()?;
103    let file_stem = path.file_stem()?.to_str()?;
104
105    // Check if this is a facade file (starts with underscore)
106    if let Some(module_name) = file_stem.strip_prefix('_') {
107        // Facade file: _utils.gruel -> parent/utils
108        // Strip the leading underscore
109        Some(parent.join(module_name))
110    } else {
111        // Regular file: the module is just the parent directory
112        Some(parent.to_path_buf())
113    }
114}